mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Name the card that ran, and say what to install
Two things the first pass got wrong. The card was named out of whichever listing came first, so a machine with both a CUDA build and a Vulkan loader could have its card named from the wrong one. Worse, whisper numbers every device it can see in one sequence while the handle carries the backend's own index: a processor-only build lent a Vulkan backend through GGML_BACKEND_PATH reports "device 1: Vulkan0", and reading that listing by the handle's digit names whatever sat in slot zero. The backend's own enumeration is asked first now, because it is the only one indexed the way the handle is, and whisper's listing is a fallback taken only when it names exactly one card. And "this build carries no graphics backend" left the reader with nowhere to go. It is the one case with a fix worth naming: whisper.cpp publishes no build that reaches a card on Linux, and program_path runs a copy from the system ahead of the downloaded one, so installing one is the whole remedy. Server.state() now says which copy is running, and the line says so only when it is Dikte's own download; a system build that cannot reach the card gets the shorter sentence, since installing it again would change nothing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019zqCqhmeaNT6m1GPp8ZWPp
This commit is contained in:
+27
-13
@@ -854,23 +854,32 @@ def _local_where(entry):
|
||||
return where + (f" ({detail})" if detail else "")
|
||||
|
||||
|
||||
def _local_note(entry):
|
||||
"""Why the card is not in use, for a setup that asked for it and got none."""
|
||||
def _local_note(name, entry):
|
||||
"""Why the card is not in use, for a setup that asked for it and got none.
|
||||
|
||||
Three answers, because the thing to do about it differs: no card here, a
|
||||
build that could never use one, and a build Dikte downloaded that could
|
||||
never use one, which is the only case with a fix worth naming.
|
||||
"""
|
||||
if not entry.get("gpu_wanted"):
|
||||
return ""
|
||||
if ggml.accel_kind({**entry, "running": True}) != "cpu":
|
||||
return ""
|
||||
return (" - the graphics card is switched on and this build carries none"
|
||||
if ggml.cpu_only_build(entry)
|
||||
else " - the graphics card is switched on and none was found")
|
||||
if not ggml.cpu_only_build(entry):
|
||||
return " - the graphics card is switched on and none was found"
|
||||
if entry.get("downloaded"):
|
||||
return (f" - the graphics card is switched on and the downloaded build "
|
||||
f"has no GPU backend; a {name}-server on your system would be "
|
||||
f"used ahead of it")
|
||||
return " - the graphics card is switched on and this build carries none"
|
||||
|
||||
|
||||
def _local_line(entry):
|
||||
def _local_line(name, entry):
|
||||
if not entry.get("running"):
|
||||
return "not loaded"
|
||||
model = entry.get("model") or ""
|
||||
return (f"loaded on {_local_where(entry)}"
|
||||
+ (f", {model}" if model else "") + _local_note(entry))
|
||||
+ (f", {model}" if model else "") + _local_note(name, entry))
|
||||
|
||||
|
||||
def _last_local(conf):
|
||||
@@ -882,12 +891,17 @@ def _last_local(conf):
|
||||
rather than a reading of a live one.
|
||||
"""
|
||||
rows = {}
|
||||
for program, used, gpu in (
|
||||
for program, used, gpu, custom in (
|
||||
(ggml.WHISPER, conf["transcribe_provider"] == "local",
|
||||
bool(conf["local_gpu"])),
|
||||
(ggml.LLAMA, conf.uses_local_llm(), bool(conf["local_llm_gpu"]))):
|
||||
bool(conf["local_gpu"]), conf["local_binary"]),
|
||||
(ggml.LLAMA, conf.uses_local_llm(), bool(conf["local_llm_gpu"]),
|
||||
conf["local_llm_binary"])):
|
||||
accel = ggml.last_accel(program)
|
||||
binary = ggml.program_path(program, custom)
|
||||
rows[program.name] = {
|
||||
# Which copy would run now, since the log does not say which one
|
||||
# wrote it; the advice only differs for Dikte's own download.
|
||||
"downloaded": ggml.is_downloaded(binary),
|
||||
# Whether one ever started here at all, which the backend cannot
|
||||
# say on its own: a server that ran and named no backend and one
|
||||
# that never ran both leave it empty.
|
||||
@@ -922,7 +936,7 @@ def cmd_status(opts):
|
||||
# from an instance too old to have been asked.
|
||||
for name, entry in (reply.get("local") or {}).items():
|
||||
if entry.get("used") or entry.get("running"):
|
||||
lines.append(f"{name + ':':11}{_local_line(entry)}")
|
||||
lines.append(f"{name + ':':11}{_local_line(name, entry)}")
|
||||
return out(opts, reply, "\n".join(lines))
|
||||
|
||||
|
||||
@@ -1016,12 +1030,12 @@ def cmd_doctor(opts):
|
||||
lines.append(f"· {name:14} the running instance is too old to say; "
|
||||
f"reload it with: dikte restart")
|
||||
elif entry.get("running"):
|
||||
lines.append(f"✓ {name:14} {_local_line(entry)}")
|
||||
lines.append(f"✓ {name:14} {_local_line(name, entry)}")
|
||||
elif live:
|
||||
lines.append(f"· {name:14} not loaded")
|
||||
elif entry.get("backend"):
|
||||
lines.append(f"· {name:14} last run on "
|
||||
f"{_local_where(entry)}{_local_note(entry)}")
|
||||
f"{_local_where(entry)}{_local_note(name, entry)}")
|
||||
elif entry.get("ran"):
|
||||
lines.append(f"· {name:14} last run said nothing about what it "
|
||||
f"was running on")
|
||||
|
||||
+28
-10
@@ -670,19 +670,23 @@ def _enumerated(text, backend, index):
|
||||
def _card_name(text, handle):
|
||||
"""The card behind a ggml handle like "Vulkan0", named the way it sells.
|
||||
|
||||
Three places carry a name and only the third is always meaningful: the
|
||||
handle says which slot, whisper's own device listing says whichever the
|
||||
backend reported, and the backend's enumeration says what the thing is
|
||||
called. Whichever of them is not just the handle again wins.
|
||||
The backend's own enumeration is asked first because it is the only listing
|
||||
indexed the way the handle is. whisper numbers every device it can see in
|
||||
one sequence, so the Vulkan card can be its device 1 while being Vulkan0,
|
||||
and reading that row by the handle's digit names whatever else was in slot
|
||||
zero. The handle itself is never an answer: it says which slot, and a line
|
||||
reading "Vulkan, Vulkan0" tells nobody which card is doing the work.
|
||||
"""
|
||||
parts = _HANDLE.match(handle or "")
|
||||
backend, index = (parts.group(1), parts.group(2) or "0") if parts else ("", "0")
|
||||
for slot, name, _kind in _WHISPER_DEVICE.findall(text):
|
||||
if slot == index and name.strip() and not _BARE.match(name.strip()):
|
||||
return name.strip()
|
||||
# The handle itself is not an answer: it says which slot, and a line
|
||||
# reading "Vulkan, Vulkan0" tells nobody which card is doing the work.
|
||||
return _enumerated(text, backend, index)
|
||||
found = _enumerated(text, backend, index)
|
||||
if found:
|
||||
return found
|
||||
# Nothing enumerated: whisper's own listing is all there is, and a single
|
||||
# named device in it can only be the one that ran.
|
||||
named = [name.strip() for _slot, name, kind in _WHISPER_DEVICE.findall(text)
|
||||
if kind != "0" and name.strip() and not _BARE.match(name.strip())]
|
||||
return named[0] if len(named) == 1 else ""
|
||||
|
||||
# The startup chatter is the first few hundred lines; the rest of the file is a
|
||||
# line per request and grows for as long as the server lives.
|
||||
@@ -805,6 +809,10 @@ class Server:
|
||||
# first, and reporting the new model beside the old process would name
|
||||
# a model this server is not running.
|
||||
self._live = {}
|
||||
# The copy that is running, resolved rather than configured: the
|
||||
# setting is usually empty, meaning whichever one program_path finds,
|
||||
# and which one it found decides what advice is worth giving.
|
||||
self._binary = ""
|
||||
# The pid this instance last wrote to its pid file, so _forget never
|
||||
# removes a file some other Dikte wrote after us.
|
||||
self._pid = 0
|
||||
@@ -864,6 +872,8 @@ class Server:
|
||||
"device": accel.device,
|
||||
"layers": accel.layers,
|
||||
"available": list(accel.available),
|
||||
"binary": self._binary if up else "",
|
||||
"downloaded": bool(up and is_downloaded(self._binary)),
|
||||
}
|
||||
|
||||
def error(self):
|
||||
@@ -891,6 +901,8 @@ class Server:
|
||||
with self._lock:
|
||||
self._proc, self._port, self._log, self._key = proc, port, log, key
|
||||
self._accel, self._live = accel, settings
|
||||
self._binary = program_path(self.program,
|
||||
settings.get("binary", ""))
|
||||
return self.base_url()
|
||||
|
||||
def _current_url(self):
|
||||
@@ -1019,6 +1031,7 @@ class Server:
|
||||
proc, self._proc = self._proc, None
|
||||
self._port, self._log, self._key = 0, "", None
|
||||
self._accel, self._live = NO_ACCEL, {}
|
||||
self._binary = ""
|
||||
self._kill(proc, gently=True)
|
||||
if proc is not None:
|
||||
self._forget()
|
||||
@@ -1207,6 +1220,11 @@ def state():
|
||||
return {server.program.name: server.state() for server in SERVERS}
|
||||
|
||||
|
||||
def is_downloaded(path):
|
||||
"""Whether `path` is a copy Dikte fetched rather than one the system has."""
|
||||
return bool(path) and _under(path, BIN_DIR)
|
||||
|
||||
|
||||
def server_log(program):
|
||||
"""Where this program's server writes, which outlives the process."""
|
||||
return DATA_DIR / f"{program.name}-server.log"
|
||||
|
||||
+10
-6
@@ -783,12 +783,16 @@ TR = {
|
||||
"Loaded on the graphics card ({detail}).":
|
||||
"Ekran kartına yüklendi ({detail}).",
|
||||
"Loaded on the processor ({detail}).": "İşlemciye yüklendi ({detail}).",
|
||||
"Loaded on the processor: this build carries no graphics backend, so the "
|
||||
"box above cannot change that. A build from your distribution, or one you "
|
||||
"point at above, may reach the card.":
|
||||
"İşlemciye yüklendi: bu sürümde ekran kartı arka ucu yok, yukarıdaki "
|
||||
"kutu bunu değiştiremez. Dağıtımınızın kendi sürümü ya da yukarıda yol "
|
||||
"göstereceğiniz bir kopya karta ulaşabilir.",
|
||||
"Loaded on the processor: the build Dikte downloaded carries no graphics "
|
||||
"backend. A {binary} from your own system is used ahead of it, so "
|
||||
"installing one is what reaches the card.":
|
||||
"İşlemciye yüklendi: Dikte'nin indirdiği sürümde ekran kartı arka ucu "
|
||||
"yok. Sistemdeki bir {binary} indirilene tercih edilir, yani karta "
|
||||
"ulaşmanın yolu onu kurmak.",
|
||||
"Loaded on the processor: this {binary} carries no graphics backend, so "
|
||||
"the box above cannot change that.":
|
||||
"İşlemciye yüklendi: bu {binary} ekran kartı arka ucu taşımıyor, "
|
||||
"yukarıdaki kutu bunu değiştiremez.",
|
||||
"Loaded on the processor: the graphics card is switched on, but none was "
|
||||
"found.":
|
||||
"İşlemciye yüklendi: ekran kartı açık, ama bulunamadı.",
|
||||
|
||||
+19
-10
@@ -691,11 +691,13 @@ class SettingsWindow(QDialog):
|
||||
def _show_local_state(self):
|
||||
"""What each model on this machine is loaded on, as it is now."""
|
||||
local = ggml.state()
|
||||
self.local_state.setText(self._local_state_text(local.get("whisper", {})))
|
||||
self.local_llm_state.setText(self._local_state_text(local.get("llama", {})))
|
||||
self.local_state.setText(
|
||||
self._local_state_text(ggml.WHISPER, local.get("whisper", {})))
|
||||
self.local_llm_state.setText(
|
||||
self._local_state_text(ggml.LLAMA, local.get("llama", {})))
|
||||
|
||||
@staticmethod
|
||||
def _local_state_text(entry):
|
||||
def _local_state_text(program, entry):
|
||||
"""One line: whether the model is loaded, and what it ended up on.
|
||||
|
||||
Four answers rather than two, because "could not tell" is a real one: a
|
||||
@@ -714,13 +716,20 @@ class SettingsWindow(QDialog):
|
||||
return t("Loaded on the graphics card ({detail}).", detail=detail)
|
||||
if not entry.get("gpu_wanted"):
|
||||
return t("Loaded on the processor ({detail}).", detail=detail)
|
||||
if ggml.cpu_only_build(entry):
|
||||
return t("Loaded on the processor: this build carries no graphics "
|
||||
"backend, so the box above cannot change that. A build "
|
||||
"from your distribution, or one you point at above, may "
|
||||
"reach the card.")
|
||||
return t("Loaded on the processor: the graphics card is switched on, "
|
||||
"but none was found.")
|
||||
if not ggml.cpu_only_build(entry):
|
||||
return t("Loaded on the processor: the graphics card is switched "
|
||||
"on, but none was found.")
|
||||
# The one case with a fix worth naming: whisper.cpp publishes no build
|
||||
# that can reach a card on most systems, and Dikte runs a system copy
|
||||
# ahead of its own, so installing one is the whole remedy.
|
||||
if entry.get("downloaded"):
|
||||
return t("Loaded on the processor: the build Dikte downloaded "
|
||||
"carries no graphics backend. A {binary} from your own "
|
||||
"system is used ahead of it, so installing one is what "
|
||||
"reaches the card.", binary=program.binary)
|
||||
return t("Loaded on the processor: this {binary} carries no graphics "
|
||||
"backend, so the box above cannot change that.",
|
||||
binary=program.binary)
|
||||
|
||||
def _scrolled(self, page):
|
||||
"""A tab that scrolls instead of growing the window to fit."""
|
||||
|
||||
@@ -806,6 +806,12 @@ class LocalModels(DikteTest):
|
||||
self.assertIn("loaded on the processor", line)
|
||||
self.assertIn("this build carries none", line)
|
||||
|
||||
def test_the_downloaded_build_is_told_where_a_working_one_comes_from(self):
|
||||
line = self.status({"whisper": self.entry(
|
||||
backend="CPU", device="CPU", available=["CPU"], downloaded=True)})
|
||||
self.assertIn("downloaded build has no GPU backend", line)
|
||||
self.assertIn("whisper-server on your system", line)
|
||||
|
||||
def test_a_card_the_build_could_have_used_says_something_else(self):
|
||||
line = self.status({"whisper": self.entry(
|
||||
backend="CPU", device="CPU", available=["CUDA", "CPU"])})
|
||||
|
||||
@@ -608,6 +608,36 @@ load_tensors: offloaded 0/29 layers to GPU
|
||||
"""
|
||||
|
||||
|
||||
# A downloaded processor-only build pointed at the system's Vulkan backend
|
||||
# through GGML_BACKEND_PATH. whisper numbers every device it can see in one
|
||||
# sequence, so the card is its device 1 while still being Vulkan0.
|
||||
WHISPER_LENT_BACKEND = """\
|
||||
load_backend: loaded CPU backend from /data/bin/whisper/libggml-cpu-haswell.so
|
||||
ggml_vulkan: Found 1 Vulkan devices:
|
||||
ggml_vulkan: 0 = AMD Radeon RX 6600 (RADV NAVI23) (radv) | uma: 0
|
||||
load_backend: loaded Vulkan backend from /usr/lib/ggml/libggml-vulkan.so
|
||||
whisper_model_load: Vulkan0 total size = 189.49 MB
|
||||
whisper_backend_init_gpu: device 0: CPU (type: 0)
|
||||
whisper_backend_init_gpu: device 1: Vulkan0 (type: 1)
|
||||
whisper_backend_init_gpu: found GPU device 1: Vulkan0 (type: 1, cnt: 0)
|
||||
whisper_backend_init_gpu: using Vulkan0 backend
|
||||
"""
|
||||
|
||||
# Two cards, and the one that ran is not the one in the slot the handle names.
|
||||
# Reading whisper's listing by the handle's digit would name the other card.
|
||||
WHISPER_TWO_CARDS = """\
|
||||
ggml_vulkan: Found 1 Vulkan devices:
|
||||
ggml_vulkan: 0 = AMD Radeon RX 6600 (RADV NAVI23) (radv) | uma: 0
|
||||
load_backend: loaded CUDA backend from /opt/whisper/libggml-cuda.so
|
||||
load_backend: loaded Vulkan backend from /opt/whisper/libggml-vulkan.so
|
||||
load_backend: loaded CPU backend from /opt/whisper/libggml-cpu-haswell.so
|
||||
whisper_model_load: Vulkan0 total size = 189.49 MB
|
||||
whisper_backend_init_gpu: device 0: NVIDIA GeForce RTX 4070 (type: 1)
|
||||
whisper_backend_init_gpu: device 1: Vulkan0 (type: 1)
|
||||
whisper_backend_init_gpu: using Vulkan0 backend
|
||||
"""
|
||||
|
||||
|
||||
class WhatItRunsOn(Local):
|
||||
"""Reading the backend back out of the log the server wrote."""
|
||||
|
||||
@@ -648,6 +678,18 @@ class WhatItRunsOn(Local):
|
||||
self.assertNotIn("(radv)",
|
||||
self.read(ggml.WHISPER, WHISPER_VULKAN).device)
|
||||
|
||||
def test_a_card_numbered_one_way_and_handled_another_is_still_named(self):
|
||||
accel = self.read(ggml.WHISPER, WHISPER_LENT_BACKEND)
|
||||
self.assertEqual(accel.backend, "Vulkan")
|
||||
self.assertEqual(accel.device, "AMD Radeon RX 6600 (RADV NAVI23)")
|
||||
|
||||
def test_the_card_named_is_the_one_the_handle_belongs_to(self):
|
||||
# whisper's device 0 is the other card. The handle is Vulkan0, and
|
||||
# Vulkan's own device 0 is the AMD one.
|
||||
accel = self.read(ggml.WHISPER, WHISPER_TWO_CARDS)
|
||||
self.assertEqual(accel.device, "AMD Radeon RX 6600 (RADV NAVI23)")
|
||||
self.assertNotIn("NVIDIA", ggml.accel_detail(accel))
|
||||
|
||||
def test_a_card_that_failed_to_start_is_not_a_card_in_use(self):
|
||||
# It was listed, it was tried, it did not work, and whisper went on
|
||||
# without it. The listing alone would have called this a graphics card.
|
||||
@@ -694,6 +736,14 @@ class WhatItRunsOn(Local):
|
||||
self.assertEqual(ggml.accel_detail(self.read(ggml.WHISPER, WHISPER_CPU)),
|
||||
"CPU")
|
||||
|
||||
def test_which_copy_is_running_decides_what_advice_is_worth_giving(self):
|
||||
mine = ggml.BIN_DIR / "whisper" / "b1" / "whisper-server"
|
||||
mine.parent.mkdir(parents=True, exist_ok=True)
|
||||
mine.write_text("#!/bin/sh\n")
|
||||
self.assertTrue(ggml.is_downloaded(str(mine)))
|
||||
self.assertFalse(ggml.is_downloaded("/usr/bin/whisper-server"))
|
||||
self.assertFalse(ggml.is_downloaded(""))
|
||||
|
||||
def test_nothing_is_running_is_not_a_backend(self):
|
||||
self.assertEqual(ggml.accel_kind({"running": False, "backend": "CUDA"}),
|
||||
"off")
|
||||
|
||||
@@ -1205,6 +1205,20 @@ class LocalModels(DikteTest):
|
||||
self.assertIn("processor", whisper)
|
||||
self.assertIn("no graphics backend", whisper)
|
||||
|
||||
def test_the_downloaded_build_is_told_what_to_install(self):
|
||||
# whisper.cpp publishes nothing that reaches a card on this system, and
|
||||
# a copy on the PATH is used ahead of Dikte's own, so that is the fix.
|
||||
whisper, _ = self.shown(backend="CPU", device="CPU", available=["CPU"],
|
||||
downloaded=True)
|
||||
self.assertIn("Dikte downloaded", whisper)
|
||||
self.assertIn("whisper-server", whisper)
|
||||
|
||||
def test_a_system_build_is_not_told_to_install_itself(self):
|
||||
whisper, _ = self.shown(backend="CPU", device="CPU", available=["CPU"],
|
||||
downloaded=False)
|
||||
self.assertIn("no graphics backend", whisper)
|
||||
self.assertNotIn("Dikte downloaded", whisper)
|
||||
|
||||
def test_a_build_that_could_have_used_one_says_the_other_thing(self):
|
||||
whisper, _ = self.shown(backend="CPU", device="CPU",
|
||||
available=["CUDA", "CPU"])
|
||||
|
||||
Reference in New Issue
Block a user