Stop stacking Ollama -cpu embed tags and verify Assistent Sqlite after seed.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@ comfy:
|
|||||||
| `ollama` | только при `LLM_RUNTIME=ollama` |
|
| `ollama` | только при `LLM_RUNTIME=ollama` |
|
||||||
| `any-llm` | при `LLM_RUNTIME=ollama` |
|
| `any-llm` | при `LLM_RUNTIME=ollama` |
|
||||||
|
|
||||||
Строки с несовпавшим `requires` пропускаются (лог), остальные ставятся как обычно. В `extensions.example.yaml` по умолчанию — **swarm-assistent** с `requires: ollama` (чат + доска Generate/Ref + persona/Cards, ветка `main`). После `seed-extensions` + рестарта SwarmUI подтягивается текущий `main` (с **0.10.11**: default chat = `default_chat` / старший тег, warm той же моделью). На `up` (и `gpu-rent seed-personas`) сидится папка `assistent-personas/` → overlay `/mnt/swarm_data/Assistent/personas/<id>/*.json`, плюс `_base/assistant.json` (`default_persona`, опционально `num_ctx` с GPU tier). Seed **не** удаляет overlay-личности, созданные в UI. Legacy `personas.json` / yaml-prompt больше не пишутся.
|
Строки с несовпавшим `requires` пропускаются (лог), остальные ставятся как обычно. В `extensions.example.yaml` по умолчанию — **swarm-assistent** с `requires: ollama` (чат + доска Generate/Ref + persona/Cards, ветка `main`). После `seed-extensions` + рестарта SwarmUI подтягивается текущий `main` (с **0.13.1+**: `CopyLocalLockFileAssemblies` копирует `Microsoft.Data.Sqlite` + `SQLitePCLRaw*` рядом с extension DLL — иначе ListMemory/ListChats/SaveChat падают; с **0.10.11**: default chat = `default_chat` / старший тег, warm той же моделью). На `up` (и `gpu-rent seed-personas`) сидится папка `assistent-personas/` → overlay `/mnt/swarm_data/Assistent/personas/<id>/*.json`, плюс `_base/assistant.json` (`default_persona`, опционально `num_ctx` с GPU tier). Seed **не** удаляет overlay-личности, созданные в UI. Legacy `personas.json` / yaml-prompt больше не пишутся.
|
||||||
|
|
||||||
В git репозитория gpu-rent не коммитить рабочий список с лишними приватными URL сверх примера.
|
В git репозитория gpu-rent не коммитить рабочий список с лишними приватными URL сверх примера.
|
||||||
|
|
||||||
|
|||||||
@@ -971,10 +971,14 @@ def pull_output_cmd() -> None:
|
|||||||
def seed_extensions_cmd() -> None:
|
def seed_extensions_cmd() -> None:
|
||||||
"""Clone/fetch extensions.yaml, затем restart swarmui."""
|
"""Clone/fetch extensions.yaml, затем restart swarmui."""
|
||||||
try:
|
try:
|
||||||
|
from gpu_rent.provision import verify_assistent_sqlite_bins
|
||||||
|
|
||||||
cfg, host = _live()
|
cfg, host = _live()
|
||||||
|
|
||||||
seed_extensions(cfg, host, log)
|
seed_extensions(cfg, host, log)
|
||||||
ensure_swarmui_running(cfg, host, log, restart=True)
|
ensure_swarmui_running(cfg, host, log, restart=True)
|
||||||
|
# Build lands Sqlite private deps beside the extension DLL (≥0.13.1).
|
||||||
|
verify_assistent_sqlite_bins(cfg, host, log)
|
||||||
except GpuRentError as exc:
|
except GpuRentError as exc:
|
||||||
_die(exc)
|
_die(exc)
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ for root in roots:
|
|||||||
for p in sorted(root.iterdir()):
|
for p in sorted(root.iterdir()):
|
||||||
if "assistent" not in p.name.lower():
|
if "assistent" not in p.name.lower():
|
||||||
continue
|
continue
|
||||||
dll_dir = p / "bin" / "Debug" / "net8.0"
|
dlls = sorted(p.glob("bin/**/SwarmAssistentExtension.dll"))
|
||||||
dll = dll_dir / "SwarmAssistentExtension.dll"
|
if not dlls:
|
||||||
sqlite_dll = dll_dir / "Microsoft.Data.Sqlite.dll"
|
# Older layout / alternate assembly name
|
||||||
|
dlls = sorted(p.glob("bin/**/*Assistent*.dll"))
|
||||||
|
dll = dlls[0] if dlls else None
|
||||||
|
dll_dir = dll.parent if dll else None
|
||||||
|
sqlite_dll = (dll_dir / "Microsoft.Data.Sqlite.dll") if dll_dir else None
|
||||||
csproj = next(p.glob("*.csproj"), None)
|
csproj = next(p.glob("*.csproj"), None)
|
||||||
tab = p / "Tabs" / "Text2Image" / "Assistent.html"
|
tab = p / "Tabs" / "Text2Image" / "Assistent.html"
|
||||||
bundle = p / "Assets" / "assistent.bundle.js"
|
bundle = p / "Assets" / "assistent.bundle.js"
|
||||||
@@ -49,10 +53,11 @@ for root in roots:
|
|||||||
"path": str(p),
|
"path": str(p),
|
||||||
"name": p.name,
|
"name": p.name,
|
||||||
"csproj": str(csproj) if csproj else None,
|
"csproj": str(csproj) if csproj else None,
|
||||||
"dll": str(dll) if dll.is_file() else None,
|
"dll": str(dll) if dll else None,
|
||||||
"dll_mtime": int(dll.stat().st_mtime) if dll.is_file() else None,
|
"dll_mtime": int(dll.stat().st_mtime) if dll else None,
|
||||||
"sqlite_dll": sqlite_dll.is_file(),
|
"dll_dir": str(dll_dir) if dll_dir else None,
|
||||||
"sqlitepcl": any(dll_dir.glob("SQLitePCLRaw*.dll")) if dll_dir.is_dir() else False,
|
"sqlite_dll": bool(sqlite_dll and sqlite_dll.is_file()),
|
||||||
|
"sqlitepcl": any(dll_dir.glob("SQLitePCLRaw*.dll")) if dll_dir else False,
|
||||||
"tab_html": tab.is_file(),
|
"tab_html": tab.is_file(),
|
||||||
"bundle_js": bundle.is_file(),
|
"bundle_js": bundle.is_file(),
|
||||||
"git_head": head or None,
|
"git_head": head or None,
|
||||||
@@ -412,6 +417,19 @@ def collect_assistent_roles(cfg: Config, *, fs: dict[str, Any] | None = None) ->
|
|||||||
missing_mem = [m for m in memory if models and m not in models]
|
missing_mem = [m for m in memory if models and m not in models]
|
||||||
if missing_chat:
|
if missing_chat:
|
||||||
ok = False
|
ok = False
|
||||||
|
from gpu_rent.llm_runtime import is_stacked_cpu_ollama_tag
|
||||||
|
|
||||||
|
stacked = [
|
||||||
|
m
|
||||||
|
for m in list(memory) + [x for x in models if isinstance(x, str)]
|
||||||
|
if isinstance(m, str) and is_stacked_cpu_ollama_tag(m)
|
||||||
|
]
|
||||||
|
if stacked:
|
||||||
|
hints.append(
|
||||||
|
"recursive *-cpu Ollama tags — roles.memory должен быть nomic-embed-text-cpu; "
|
||||||
|
"ollama rm … затем re-up / provision_llm"
|
||||||
|
)
|
||||||
|
ok = False
|
||||||
return {
|
return {
|
||||||
"ok": ok,
|
"ok": ok,
|
||||||
"roles": {
|
"roles": {
|
||||||
|
|||||||
@@ -173,6 +173,96 @@ def already_have_ollama_tag(have: set[str], wanted: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ollama_model_root(name: str) -> str:
|
||||||
|
"""Strip ``:tag`` and any trailing ``-cpu`` suffixes.
|
||||||
|
|
||||||
|
``nomic-embed-text-cpu-cpu:latest`` → ``nomic-embed-text``.
|
||||||
|
"""
|
||||||
|
base = str(name or "").split(":", 1)[0].strip()
|
||||||
|
while base.endswith("-cpu"):
|
||||||
|
base = base[: -len("-cpu")]
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def is_cpu_ollama_tag(name: str) -> bool:
|
||||||
|
"""True if the model name (sans ``:tag``) ends with ``-cpu``."""
|
||||||
|
return str(name or "").split(":", 1)[0].endswith("-cpu")
|
||||||
|
|
||||||
|
|
||||||
|
def is_stacked_cpu_ollama_tag(name: str) -> bool:
|
||||||
|
"""True for recursive junk like ``nomic-embed-text-cpu-cpu``."""
|
||||||
|
base = str(name or "").split(":", 1)[0]
|
||||||
|
if not base.endswith("-cpu"):
|
||||||
|
return False
|
||||||
|
return base[: -len("-cpu")].endswith("-cpu")
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_ollama_tag(name: str) -> str:
|
||||||
|
"""Single CPU variant tag: ``nomic-embed-text`` / ``…-cpu-cpu`` → ``nomic-embed-text-cpu``."""
|
||||||
|
root = ollama_model_root(name)
|
||||||
|
return f"{root}-cpu" if root else ""
|
||||||
|
|
||||||
|
|
||||||
|
def pick_ollama_from_tag(have: set[str], wanted: str) -> str | None:
|
||||||
|
"""Pick a FROM tag for a CPU Modelfile — prefer the non-cpu library pull."""
|
||||||
|
root = ollama_model_root(wanted)
|
||||||
|
if not root:
|
||||||
|
return None
|
||||||
|
for candidate in (root, f"{root}:latest"):
|
||||||
|
if already_have_ollama_tag(have, candidate):
|
||||||
|
if candidate in have:
|
||||||
|
return candidate
|
||||||
|
if f"{root}:latest" in have:
|
||||||
|
return f"{root}:latest"
|
||||||
|
if root in have:
|
||||||
|
return root
|
||||||
|
non_cpu: list[str] = []
|
||||||
|
single_cpu: list[str] = []
|
||||||
|
for tag in sorted(have):
|
||||||
|
if ollama_model_root(tag) != root:
|
||||||
|
continue
|
||||||
|
if is_stacked_cpu_ollama_tag(tag):
|
||||||
|
continue
|
||||||
|
if is_cpu_ollama_tag(tag):
|
||||||
|
single_cpu.append(tag)
|
||||||
|
else:
|
||||||
|
non_cpu.append(tag)
|
||||||
|
if non_cpu:
|
||||||
|
return non_cpu[0]
|
||||||
|
if single_cpu:
|
||||||
|
return single_cpu[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def finalize_memory_role_tags(
|
||||||
|
memory: list[str],
|
||||||
|
*,
|
||||||
|
prefer_cpu: bool = True,
|
||||||
|
) -> list[str]:
|
||||||
|
"""One tag per embed root; collapse stacked ``-cpu`` names; optionally prefer ``*-cpu``."""
|
||||||
|
by_root: dict[str, str] = {}
|
||||||
|
for raw in memory:
|
||||||
|
name = str(raw or "").strip()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
root = ollama_model_root(name)
|
||||||
|
if not root:
|
||||||
|
continue
|
||||||
|
curated = cpu_ollama_tag(name) if is_cpu_ollama_tag(name) else name
|
||||||
|
# Always normalize stacked cpu → single ``*-cpu``
|
||||||
|
if is_stacked_cpu_ollama_tag(name):
|
||||||
|
curated = cpu_ollama_tag(name)
|
||||||
|
cur = by_root.get(root)
|
||||||
|
if cur is None:
|
||||||
|
by_root[root] = curated
|
||||||
|
continue
|
||||||
|
if prefer_cpu and is_cpu_ollama_tag(curated) and not is_cpu_ollama_tag(cur):
|
||||||
|
by_root[root] = curated
|
||||||
|
elif is_stacked_cpu_ollama_tag(cur) and not is_stacked_cpu_ollama_tag(curated):
|
||||||
|
by_root[root] = curated
|
||||||
|
return list(by_root.values())
|
||||||
|
|
||||||
|
|
||||||
def preferred_ollama_model(path: Path) -> str | None:
|
def preferred_ollama_model(path: Path) -> str | None:
|
||||||
"""Manifest default chat model, else first chat tag (never memory/embed)."""
|
"""Manifest default chat model, else first chat tag (never memory/embed)."""
|
||||||
entries = [e for e in parse_ollama_models(path) if e.use == "chat"]
|
entries = [e for e in parse_ollama_models(path) if e.use == "chat"]
|
||||||
@@ -185,7 +275,10 @@ def preferred_ollama_model(path: Path) -> str | None:
|
|||||||
def ollama_roles_payload(entries: list[OllamaModelEntry]) -> dict[str, list[str] | str | None]:
|
def ollama_roles_payload(entries: list[OllamaModelEntry]) -> dict[str, list[str] | str | None]:
|
||||||
"""Sidecar for Assistent: chat/memory lists + preferred default_chat (manifest default: true)."""
|
"""Sidecar for Assistent: chat/memory lists + preferred default_chat (manifest default: true)."""
|
||||||
chat = [e.name for e in entries if e.use == "chat"]
|
chat = [e.name for e in entries if e.use == "chat"]
|
||||||
memory = [e.name for e in entries if e.use == "memory"]
|
memory = finalize_memory_role_tags(
|
||||||
|
[e.name for e in entries if e.use == "memory"],
|
||||||
|
prefer_cpu=False,
|
||||||
|
)
|
||||||
preferred = next((e.name for e in entries if e.use == "chat" and e.default), None)
|
preferred = next((e.name for e in entries if e.use == "chat" and e.default), None)
|
||||||
if preferred is None and chat:
|
if preferred is None and chat:
|
||||||
preferred = chat[0]
|
preferred = chat[0]
|
||||||
|
|||||||
+190
-34
@@ -197,7 +197,144 @@ def seed_extensions(cfg: Config, host: str, log: Log, *, update: bool = True) ->
|
|||||||
timeout=1800,
|
timeout=1800,
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
return "cloned " in out or "updated " in out
|
changed = "cloned " in out or "updated " in out
|
||||||
|
_log_assistent_source_sqlite_hint(cfg, host, log)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
_ASSISTENT_SQLITE_SRC_PY = r"""
|
||||||
|
from pathlib import Path
|
||||||
|
roots = [Path("/mnt/swarm_data/Extensions"), Path("/opt/swarmui/src/Extensions")]
|
||||||
|
found = []
|
||||||
|
for root in roots:
|
||||||
|
if not root.is_dir():
|
||||||
|
continue
|
||||||
|
for p in sorted(root.iterdir()):
|
||||||
|
if "assistent" not in p.name.lower():
|
||||||
|
continue
|
||||||
|
csproj = next(p.glob("*.csproj"), None)
|
||||||
|
text = csproj.read_text(encoding="utf-8", errors="replace") if csproj else ""
|
||||||
|
found.append({
|
||||||
|
"path": str(p),
|
||||||
|
"name": p.name,
|
||||||
|
"has_csproj": bool(csproj),
|
||||||
|
"has_sqlite_pkg": "Microsoft.Data.Sqlite" in text,
|
||||||
|
"has_copy_local": "CopyLocalLockFileAssemblies" in text,
|
||||||
|
"has_ship_target": "ShipOnlySqlitePrivateDeps" in text,
|
||||||
|
})
|
||||||
|
print(__import__("json").dumps(found))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
_ASSISTENT_SQLITE_BIN_PY = r"""
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
roots = [Path("/mnt/swarm_data/Extensions"), Path("/opt/swarmui/src/Extensions")]
|
||||||
|
found = []
|
||||||
|
for root in roots:
|
||||||
|
if not root.is_dir():
|
||||||
|
continue
|
||||||
|
for p in sorted(root.iterdir()):
|
||||||
|
if "assistent" not in p.name.lower():
|
||||||
|
continue
|
||||||
|
dlls = sorted(p.glob("bin/**/SwarmAssistentExtension.dll"))
|
||||||
|
dll = dlls[0] if dlls else None
|
||||||
|
dll_dir = dll.parent if dll else None
|
||||||
|
sqlite = (dll_dir / "Microsoft.Data.Sqlite.dll") if dll_dir else None
|
||||||
|
pcl = list(dll_dir.glob("SQLitePCLRaw*.dll")) if dll_dir else []
|
||||||
|
found.append({
|
||||||
|
"path": str(p),
|
||||||
|
"name": p.name,
|
||||||
|
"dll": str(dll) if dll else None,
|
||||||
|
"sqlite_dll": bool(sqlite and sqlite.is_file()),
|
||||||
|
"sqlitepcl": bool(pcl),
|
||||||
|
})
|
||||||
|
print(json.dumps(found))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _log_assistent_source_sqlite_hint(cfg: Config, host: str, log: Log) -> None:
|
||||||
|
"""After clone: warn if Assistent csproj lacks Sqlite private-dep wiring (≥0.13.1)."""
|
||||||
|
try:
|
||||||
|
raw = run_ssh(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
"python3 - <<'PY'\n" + _ASSISTENT_SQLITE_SRC_PY + "\nPY",
|
||||||
|
check=False,
|
||||||
|
timeout=20,
|
||||||
|
).strip()
|
||||||
|
if not raw:
|
||||||
|
return
|
||||||
|
rows = json.loads(raw.splitlines()[-1])
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if not isinstance(rows, list) or not rows:
|
||||||
|
return
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
name = row.get("name") or "assistent"
|
||||||
|
if not row.get("has_csproj"):
|
||||||
|
continue
|
||||||
|
ok = (
|
||||||
|
row.get("has_sqlite_pkg")
|
||||||
|
and row.get("has_copy_local")
|
||||||
|
and row.get("has_ship_target")
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
log(
|
||||||
|
f"Assistent {name}: csproj Sqlite private deps OK "
|
||||||
|
"(нужен restart SwarmUI, чтобы dll+Sqlite оказались в bin/)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log(
|
||||||
|
f"⚠ Assistent {name}: csproj без Sqlite private deps "
|
||||||
|
"(нужен swarm-assistent ≥0.13.1 на main — seed-extensions с update)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_assistent_sqlite_bins(cfg: Config, host: str, log: Log) -> bool:
|
||||||
|
"""After SwarmUI build/restart: Microsoft.Data.Sqlite (+ SQLitePCLRaw) beside extension DLL."""
|
||||||
|
try:
|
||||||
|
raw = run_ssh(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
"python3 - <<'PY'\n" + _ASSISTENT_SQLITE_BIN_PY + "\nPY",
|
||||||
|
check=False,
|
||||||
|
timeout=25,
|
||||||
|
).strip()
|
||||||
|
if not raw:
|
||||||
|
return False
|
||||||
|
rows = json.loads(raw.splitlines()[-1])
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"⚠ Assistent Sqlite bin check: {exc}")
|
||||||
|
return False
|
||||||
|
if not isinstance(rows, list) or not rows:
|
||||||
|
log("⚠ Assistent: extension dir не найден — seed-extensions?")
|
||||||
|
return False
|
||||||
|
ok_any = False
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
name = row.get("name") or "assistent"
|
||||||
|
if not row.get("dll"):
|
||||||
|
log(
|
||||||
|
f"⚠ Assistent {name}: DLL ещё нет в bin/ — подожди compile после restart "
|
||||||
|
"или смотри journalctl -u swarmui"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if row.get("sqlite_dll") and row.get("sqlitepcl"):
|
||||||
|
log(f"Assistent {name}: Microsoft.Data.Sqlite + SQLitePCLRaw рядом с DLL")
|
||||||
|
ok_any = True
|
||||||
|
elif row.get("sqlite_dll"):
|
||||||
|
log(f"Assistent {name}: Microsoft.Data.Sqlite есть (SQLitePCLRaw не найден)")
|
||||||
|
ok_any = True
|
||||||
|
else:
|
||||||
|
log(
|
||||||
|
f"⚠ Assistent {name}: DLL есть, но нет Microsoft.Data.Sqlite.dll — "
|
||||||
|
"чат/memory API упадут. Нужен ≥0.13.1 + seed-extensions + restart"
|
||||||
|
)
|
||||||
|
return ok_any
|
||||||
|
|
||||||
|
|
||||||
def _github_blob(cfg: Config) -> dict | None:
|
def _github_blob(cfg: Config) -> dict | None:
|
||||||
@@ -1193,10 +1330,16 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None:
|
|||||||
from gpu_rent.llm_runtime import (
|
from gpu_rent.llm_runtime import (
|
||||||
MEMORY_EMBED_MODEL,
|
MEMORY_EMBED_MODEL,
|
||||||
already_have_ollama_tag,
|
already_have_ollama_tag,
|
||||||
|
cpu_ollama_tag,
|
||||||
ensure_memory_model_entries,
|
ensure_memory_model_entries,
|
||||||
|
finalize_memory_role_tags,
|
||||||
|
is_cpu_ollama_tag,
|
||||||
|
is_stacked_cpu_ollama_tag,
|
||||||
normalize_runtime,
|
normalize_runtime,
|
||||||
|
ollama_model_root,
|
||||||
ollama_roles_payload,
|
ollama_roles_payload,
|
||||||
parse_ollama_models,
|
parse_ollama_models,
|
||||||
|
pick_ollama_from_tag,
|
||||||
preferred_ollama_model,
|
preferred_ollama_model,
|
||||||
)
|
)
|
||||||
from gpu_rent.ssh_ops import run_script_sudo
|
from gpu_rent.ssh_ops import run_script_sudo
|
||||||
@@ -1283,31 +1426,30 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None:
|
|||||||
# Sidecar roles for Assistent (chat vs memory selects)
|
# Sidecar roles for Assistent (chat vs memory selects)
|
||||||
roles = ollama_roles_payload(entries)
|
roles = ollama_roles_payload(entries)
|
||||||
run_ssh(cfg, host, f"mkdir -p {DATA}/Assistent", check=False)
|
run_ssh(cfg, host, f"mkdir -p {DATA}/Assistent", check=False)
|
||||||
put_text(
|
# Pin memory models to CPU (num_gpu 0) so they run beside chat VL.
|
||||||
cfg,
|
# Never stack ``-cpu`` onto an already-cpu tag (re-provision used to
|
||||||
host,
|
# create nomic-embed-text-cpu-cpu-… and point roles.memory at junk).
|
||||||
f"{DATA}/Assistent/ollama-roles.json",
|
mem_roots: list[str] = []
|
||||||
json.dumps(roles, indent=2, ensure_ascii=False) + "\n",
|
|
||||||
)
|
|
||||||
log(f"Assistent ollama-roles: chat={len(roles['chat'])} memory={len(roles['memory'])}")
|
|
||||||
# Pin memory models to CPU (num_gpu 0) so they run beside chat VL
|
|
||||||
for mem in roles["memory"] or [MEMORY_EMBED_MODEL]:
|
for mem in roles["memory"] or [MEMORY_EMBED_MODEL]:
|
||||||
if not already_have_ollama_tag(have, mem) and not already_have_ollama_tag(
|
root = ollama_model_root(str(mem))
|
||||||
have, mem.split(":")[0]
|
if root and root not in mem_roots:
|
||||||
):
|
mem_roots.append(root)
|
||||||
|
memory_out: list[str] = []
|
||||||
|
for root in mem_roots:
|
||||||
|
cpu_tag = cpu_ollama_tag(root)
|
||||||
|
from_tag = pick_ollama_from_tag(have, root)
|
||||||
|
if from_tag is None:
|
||||||
|
log(f"⚠ memory model {root} нет в /api/tags — skip CPU pin")
|
||||||
|
memory_out.append(root)
|
||||||
continue
|
continue
|
||||||
base = mem
|
|
||||||
# Prefer exact tag present in /api/tags
|
|
||||||
for tag in sorted(have):
|
|
||||||
if tag == mem or tag.startswith(mem.split(":")[0]):
|
|
||||||
base = tag
|
|
||||||
break
|
|
||||||
cpu_tag = f"{base.split(':')[0]}-cpu"
|
|
||||||
if already_have_ollama_tag(have, cpu_tag):
|
if already_have_ollama_tag(have, cpu_tag):
|
||||||
if cpu_tag not in roles["memory"]:
|
memory_out.append(cpu_tag)
|
||||||
roles["memory"].append(cpu_tag)
|
|
||||||
continue
|
continue
|
||||||
modelfile = f"FROM {base}\nPARAMETER num_gpu 0\n"
|
if is_cpu_ollama_tag(from_tag):
|
||||||
|
# Only a (single) cpu variant exists — do not create *-cpu-cpu
|
||||||
|
memory_out.append(cpu_ollama_tag(from_tag))
|
||||||
|
continue
|
||||||
|
modelfile = f"FROM {from_tag}\nPARAMETER num_gpu 0\n"
|
||||||
put_text(cfg, host, "/tmp/gpu-rent-embed.Modelfile", modelfile)
|
put_text(cfg, host, "/tmp/gpu-rent-embed.Modelfile", modelfile)
|
||||||
try:
|
try:
|
||||||
run_ssh(
|
run_ssh(
|
||||||
@@ -1317,20 +1459,30 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None:
|
|||||||
timeout=300,
|
timeout=300,
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
roles["memory"] = [
|
have.add(cpu_tag)
|
||||||
cpu_tag if x == mem or x == base else x for x in roles["memory"]
|
memory_out.append(cpu_tag)
|
||||||
]
|
log(f"Ollama memory CPU model: {cpu_tag} (from {from_tag})")
|
||||||
if cpu_tag not in roles["memory"]:
|
|
||||||
roles["memory"].append(cpu_tag)
|
|
||||||
put_text(
|
|
||||||
cfg,
|
|
||||||
host,
|
|
||||||
f"{DATA}/Assistent/ollama-roles.json",
|
|
||||||
json.dumps(roles, indent=2, ensure_ascii=False) + "\n",
|
|
||||||
)
|
|
||||||
log(f"Ollama memory CPU model: {cpu_tag} (from {base})")
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"⚠ Ollama create {cpu_tag}: {exc}")
|
log(f"⚠ Ollama create {cpu_tag}: {exc}")
|
||||||
|
memory_out.append(root)
|
||||||
|
roles["memory"] = finalize_memory_role_tags(memory_out, prefer_cpu=True)
|
||||||
|
put_text(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
f"{DATA}/Assistent/ollama-roles.json",
|
||||||
|
json.dumps(roles, indent=2, ensure_ascii=False) + "\n",
|
||||||
|
)
|
||||||
|
log(
|
||||||
|
f"Assistent ollama-roles: chat={len(roles['chat'])} "
|
||||||
|
f"memory={roles['memory']}"
|
||||||
|
)
|
||||||
|
junk = sorted(t for t in have if is_stacked_cpu_ollama_tag(t))
|
||||||
|
if junk:
|
||||||
|
log(
|
||||||
|
"⚠ Ollama recursive *-cpu tags (можно удалить: ollama rm …): "
|
||||||
|
+ ", ".join(junk[:6])
|
||||||
|
+ ("…" if len(junk) > 6 else "")
|
||||||
|
)
|
||||||
warm = preferred_ollama_model(cfg.ollama_models_manifest) or next(
|
warm = preferred_ollama_model(cfg.ollama_models_manifest) or next(
|
||||||
(
|
(
|
||||||
n
|
n
|
||||||
@@ -1484,6 +1636,10 @@ def provision_vm(
|
|||||||
|
|
||||||
if swarm:
|
if swarm:
|
||||||
ensure_swarmui_running(cfg, host, log, restart=restart)
|
ensure_swarmui_running(cfg, host, log, restart=restart)
|
||||||
|
try:
|
||||||
|
verify_assistent_sqlite_bins(cfg, host, log)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"Assistent Sqlite check: {exc}")
|
||||||
run_ssh(
|
run_ssh(
|
||||||
cfg,
|
cfg,
|
||||||
host,
|
host,
|
||||||
|
|||||||
@@ -54,3 +54,56 @@ def test_write_preset_includes_memory(tmp_path: Path):
|
|||||||
entries = parse_ollama_models(path)
|
entries = parse_ollama_models(path)
|
||||||
assert preferred_ollama_model(path) is not None
|
assert preferred_ollama_model(path) is not None
|
||||||
assert preferred_ollama_model(path) != MEMORY_EMBED_MODEL
|
assert preferred_ollama_model(path) != MEMORY_EMBED_MODEL
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_ollama_tag_does_not_stack():
|
||||||
|
from gpu_rent.llm_runtime import (
|
||||||
|
cpu_ollama_tag,
|
||||||
|
finalize_memory_role_tags,
|
||||||
|
is_stacked_cpu_ollama_tag,
|
||||||
|
ollama_model_root,
|
||||||
|
pick_ollama_from_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ollama_model_root("nomic-embed-text-cpu-cpu-cpu") == "nomic-embed-text"
|
||||||
|
assert cpu_ollama_tag("nomic-embed-text") == "nomic-embed-text-cpu"
|
||||||
|
assert cpu_ollama_tag("nomic-embed-text-cpu") == "nomic-embed-text-cpu"
|
||||||
|
assert cpu_ollama_tag("nomic-embed-text-cpu-cpu:latest") == "nomic-embed-text-cpu"
|
||||||
|
assert is_stacked_cpu_ollama_tag("nomic-embed-text-cpu-cpu")
|
||||||
|
assert not is_stacked_cpu_ollama_tag("nomic-embed-text-cpu")
|
||||||
|
|
||||||
|
have = {
|
||||||
|
"nomic-embed-text",
|
||||||
|
"nomic-embed-text-cpu",
|
||||||
|
"nomic-embed-text-cpu-cpu",
|
||||||
|
"nomic-embed-text-cpu-cpu-cpu",
|
||||||
|
}
|
||||||
|
assert pick_ollama_from_tag(have, "nomic-embed-text") == "nomic-embed-text"
|
||||||
|
assert pick_ollama_from_tag(have, "nomic-embed-text-cpu") == "nomic-embed-text"
|
||||||
|
# Re-provision must not FROM a stacked tag
|
||||||
|
assert pick_ollama_from_tag(
|
||||||
|
{"nomic-embed-text-cpu-cpu", "nomic-embed-text-cpu"},
|
||||||
|
"nomic-embed-text-cpu",
|
||||||
|
) == "nomic-embed-text-cpu"
|
||||||
|
|
||||||
|
assert finalize_memory_role_tags(
|
||||||
|
["nomic-embed-text", "nomic-embed-text-cpu-cpu-cpu"],
|
||||||
|
prefer_cpu=True,
|
||||||
|
) == ["nomic-embed-text-cpu"]
|
||||||
|
assert finalize_memory_role_tags(
|
||||||
|
["nomic-embed-text-cpu-cpu", "nomic-embed-text"],
|
||||||
|
prefer_cpu=True,
|
||||||
|
) == ["nomic-embed-text-cpu"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_roles_payload_collapses_duplicate_memory_roots():
|
||||||
|
from gpu_rent.llm_runtime import OllamaModelEntry, ollama_roles_payload
|
||||||
|
|
||||||
|
roles = ollama_roles_payload(
|
||||||
|
[
|
||||||
|
OllamaModelEntry(name="chat:7b", default=True, use="chat"),
|
||||||
|
OllamaModelEntry(name="nomic-embed-text", use="memory"),
|
||||||
|
OllamaModelEntry(name="nomic-embed-text-cpu-cpu", use="memory"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert roles["memory"] == ["nomic-embed-text"]
|
||||||
|
|||||||
Reference in New Issue
Block a user