Group the model lists and say which row this machine should take

The two local model boxes handed over a flat list sorted by size and left
every choice in it to the reader. For whisper that interleaved the models:
large-v3-turbo-q5_0 landed between the two medium quantisations, half a
screen from the turbo model it is a copy of. For cleanup it was forty
repository ids, half of which answer with nothing at all because what they
publish is split across files or larger than the cap, and an empty box read
as though the click had not registered.

Now each box says what the machine is, groups the list by model, and marks
the row to take:

- whisper rows are grouped by model, with the quantisations and the
  English-only files under the model they are a copy of, and every row says
  its bit depth rather than leaving q5_1 and Q4_K_M and BF16 to be decoded.
- the recommendation follows the machine. Under 4 GB it is small-q5_1;
  with a graphics interface and 15 GB it is large-v3-q5_0, which is worth
  about two and a half points of word error in the languages that are not
  English; in between it is turbo, and a processor build where the Vulkan
  one belongs is not counted as a card.
- a row larger than half the memory less a gigabyte says it is too big.
- the publisher box holds the five suggestions until the switch beside it
  is turned on, and a line under it says in words what the chosen one is.
- a publisher that answers with nothing says why instead of going blank.
- the draft heads (dflash, dspark, eagle3) are no longer offered as models,
  and neither are the base models that sit beside their tuned twin.
This commit is contained in:
2026-09-05 11:12:42 +03:00
parent 4d0c0e29f1
commit 08fc2e4a9d
7 changed files with 1021 additions and 48 deletions
+215
View File
@@ -49,6 +49,11 @@ def item(name, data, url="https://example.invalid/f", sha=True):
hashlib.sha256(data).hexdigest() if sha else "")
def listed(name, size):
"""A row as a listing hands it over: a name and a size, no bytes."""
return hub.Item(name, f"https://example.invalid/{name}", size, "a" * 64)
@contextlib.contextmanager
def serving(release, archive):
"""Answer by what is being asked for rather than by what came before.
@@ -664,6 +669,47 @@ class Catalogue(Local):
with self.assertRaises(ggml.LocalError):
ggml.whisper_models()
def test_the_speculative_decoding_heads_are_not_models(self):
# They are the small files in a repository, so a list sorted by size
# puts them first, where the eye lands and the click goes.
tree = GGUF_TREE + [
{"type": "file", "path": "dflash-Qwen3-8B-Q8_0.gguf",
"size": 1_120_000_000, "lfs": {"oid": "f" * 64}},
{"type": "file", "path": "eagle3-gpt-oss-20b-Q8_0.gguf",
"size": 920_000_000, "lfs": {"oid": "0" * 64}},
]
with fake_urlopen(tree):
names = [q.name for q in ggml.llm_quants("ggml-org/x-GGUF")]
self.assertEqual(names,
["gemma-3-4b-it-Q4_K_M.gguf", "gemma-3-4b-it-Q8_0.gguf"])
def test_a_speech_or_vision_repository_is_not_a_cleanup_publisher(self):
listing = [{"id": "ggml-org/parakeet-GGUF"},
{"id": "ggml-org/Qwen3-TTS-12Hz-1.7B-Base-GGUF"},
{"id": "ggml-org/SmolVLM2-256M-Video-Instruct-GGUF"},
{"id": "ggml-org/Qwen3-8B-Base-GGUF"},
{"id": "ggml-org/SmolLM3-3B-GGUF"}]
with fake_urlopen(listing):
found = ggml.llm_repos()
self.assertEqual([r for r in found if r.startswith("ggml-org/Smol")],
["ggml-org/SmolLM3-3B-GGUF"])
self.assertNotIn("ggml-org/parakeet-GGUF", found)
self.assertNotIn("ggml-org/Qwen3-8B-Base-GGUF", found)
def test_a_base_model_beside_its_tuned_twin_is_dropped(self):
# Gemma names the base model after the tuned one with the `-it` taken
# out, so the two sit next to each other and the wrong one answers a
# cleanup prompt by carrying on writing the transcript.
listing = [{"id": "ggml-org/gemma-4-E2B-GGUF"},
{"id": "ggml-org/gemma-4-E2B-it-GGUF"},
{"id": "ggml-org/Qwen3-0.6B-GGUF"}]
with fake_urlopen(listing):
found = ggml.llm_repos()
self.assertNotIn("ggml-org/gemma-4-E2B-GGUF", found)
self.assertIn("ggml-org/gemma-4-E2B-it-GGUF", found)
# Nothing named it, so nothing says it is the wrong half of a pair.
self.assertIn("ggml-org/Qwen3-0.6B-GGUF", found)
def test_what_is_on_disk_is_read_from_disk(self):
self.assertEqual(ggml.installed_whisper_models(), [])
path = ggml.whisper_model_path("ggml-base.bin")
@@ -1183,3 +1229,172 @@ class WindowsOwnership(Local):
# from here", and only one of those makes the pid file safe to drop.
self.image("")
self.assertIsNone(self.made._is_ours(1234))
class Machine(Local):
"""What this machine can hold, and what that makes worth pointing at."""
def test_the_memory_is_read_the_way_each_system_reports_it(self):
# Linux and most Macs answer through sysconf.
with mock.patch.object(ggml.os, "sysconf", lambda name:
4096 if name == "SC_PAGE_SIZE" else 4_194_304):
self.assertEqual(ggml.total_memory(), 16 * ggml.GB)
def test_a_mac_without_the_page_count_is_asked_for_the_number(self):
# Not every build of Python on a Mac carries SC_PHYS_PAGES, and a Mac
# that answered nothing would be a Mac with none of this on it.
def answer(args, **kwargs):
self.assertEqual(args, ["sysctl", "-n", "hw.memsize"])
return mock.Mock(stdout=f"{32 * ggml.GB}\n")
with mock.patch.object(ggml.os, "sysconf", side_effect=ValueError), \
mock.patch.object(sys, "platform", "darwin"), \
mock.patch.object(ggml.subprocess, "run", answer):
self.assertEqual(ggml.total_memory(), 32 * ggml.GB)
def test_a_system_that_answers_nothing_is_an_unknown_machine(self):
with mock.patch.object(ggml.os, "sysconf", side_effect=ValueError), \
mock.patch.object(sys, "platform", "linux"):
self.assertEqual(ggml.total_memory(), 0)
def test_a_mac_is_taken_to_have_a_graphics_interface(self):
with mock.patch.object(sys, "platform", "darwin"):
self.assertEqual(ggml.accelerator(), "Metal")
def test_elsewhere_the_vulkan_loader_is_what_says_so(self):
with mock.patch.object(sys, "platform", "linux"), \
mock.patch.object(ggml.ctypes.util, "find_library",
lambda name: "/usr/lib/libvulkan.so.1"):
self.assertEqual(ggml.accelerator(), "Vulkan")
with mock.patch.object(sys, "platform", "linux"), \
mock.patch.object(ggml.ctypes.util, "find_library",
lambda name: None):
self.assertEqual(ggml.accelerator(), "")
def test_a_model_is_measured_against_half_the_memory(self):
self.assertTrue(ggml.fits(2 * ggml.GB, memory=8 * ggml.GB))
self.assertFalse(ggml.fits(4 * ggml.GB, memory=8 * ggml.GB))
def test_a_machine_whose_memory_could_not_be_read_holds_anything(self):
# A wrong "too big" is worse advice than none.
self.assertTrue(ggml.fits(40 * ggml.GB, memory=0))
def test_the_smallest_machine_is_not_the_one_where_everything_fits(self):
# Half of 2 GB less the gigabyte of overhead is nothing, and a budget
# of nothing used to read as the unknown machine above.
self.assertFalse(ggml.fits(3 * ggml.GB, memory=2 * ggml.GB))
def test_a_crowded_machine_is_pointed_at_the_smaller_model(self):
self.assertEqual(ggml.suggested_whisper(memory=3 * ggml.GB, graphics=""),
ggml.SMALL_MACHINE_WHISPER)
def test_a_card_and_the_memory_for_it_are_pointed_at_the_accurate_one(self):
self.assertEqual(
ggml.suggested_whisper(memory=32 * ggml.GB, graphics="Vulkan"),
ggml.ACCURATE_WHISPER)
def test_memory_without_a_card_is_pointed_at_the_fast_one(self):
# Several times the work per second is several times a long wait on a
# processor, whatever there is room for.
self.assertEqual(
ggml.suggested_whisper(memory=32 * ggml.GB, graphics=""),
ggml.SUGGESTED_WHISPER)
def test_a_sixteen_gigabyte_machine_counts_as_a_roomy_one(self):
# What a machine reports is what the firmware and the graphics left
# of it: 16 GB answers about 15.4, and a threshold written at the
# number on the box is one no machine ever reaches.
self.assertEqual(
ggml.suggested_whisper(memory=int(15.4 * ggml.GB), graphics="Metal"),
ggml.ACCURATE_WHISPER)
def test_the_suggestion_that_fits_is_offered_first(self):
first = ggml.suggested_llm(memory=6 * ggml.GB)[0]
self.assertTrue(ggml.fits(ggml.SUGGESTED_LLM_SIZE[first],
memory=6 * ggml.GB))
# Nothing is dropped: what does not fit today fits once something else
# is closed.
self.assertEqual(sorted(ggml.suggested_llm(memory=6 * ggml.GB)),
sorted(ggml.SUGGESTED_LLM))
def test_the_wanted_model_wins_when_there_is_room_for_it(self):
items = [listed("ggml-tiny.bin", 70 << 20),
listed("ggml-large-v3-turbo-q5_0.bin", 574 << 20)]
self.assertEqual(
ggml.recommended(items, "ggml-large-v3-turbo-q5_0.bin",
memory=16 * ggml.GB),
"ggml-large-v3-turbo-q5_0.bin")
def test_a_model_too_big_for_the_machine_is_not_recommended(self):
items = [listed("small.gguf", 1 << 30), listed("huge.gguf", 12 * ggml.GB)]
self.assertEqual(ggml.recommended(items, "huge.gguf",
memory=8 * ggml.GB), "small.gguf")
def test_the_full_precision_weights_are_never_the_recommendation(self):
# Twice the memory and twice the wait for a difference this job
# cannot see.
items = [listed("model-Q4_0.gguf", 2 * ggml.GB),
listed("model-BF16.gguf", 3 * ggml.GB)]
self.assertEqual(ggml.recommended(items, memory=32 * ggml.GB),
"model-Q4_0.gguf")
def test_nothing_is_recommended_when_nothing_fits(self):
self.assertEqual(
ggml.recommended([listed("huge.gguf", 40 * ggml.GB)],
memory=8 * ggml.GB), "")
class Grouping(Local):
"""One group per model, rather than one long list sorted by size."""
def test_every_spelling_of_a_quantisation_reads_as_its_number(self):
# One list holds q5_1, Q4_K_M, MXFP4 and BF16, and the number is the
# whole of what any of them says to somebody choosing a row.
self.assertEqual(ggml.bit_depth("ggml-small-q5_1.bin"), 5)
self.assertEqual(ggml.bit_depth("SmolLM3-Q4_K_M.gguf"), 4)
self.assertEqual(ggml.bit_depth("gpt-oss-20b-MXFP4.gguf"), 4)
self.assertEqual(ggml.bit_depth("gemma-4-E2B-it-Q8_0.gguf"), 8)
# bf16 is not f16 read badly.
self.assertEqual(ggml.bit_depth("gemma-4-E2B-it-BF16.gguf"), 16)
self.assertEqual(ggml.bit_depth("mmproj-model-f16.gguf"), 16)
# A whisper file with no mark is the full model, and its name is the
# one convention here that does not carry the answer.
self.assertEqual(ggml.bit_depth("ggml-large-v3-turbo.bin"), 0)
def test_a_quantisation_belongs_to_the_model_it_is_a_copy_of(self):
self.assertEqual(ggml.whisper_family("ggml-small.en-q5_1.bin"), "small")
self.assertEqual(ggml.whisper_family("ggml-large-v3-q5_0.bin"),
"large-v3")
self.assertEqual(ggml.whisper_family("ggml-large-v3-turbo.bin"),
"large-v3-turbo")
self.assertEqual(ggml.whisper_family("ggml-medium.en.bin"), "medium")
def test_turbo_is_a_model_and_not_a_quantisation(self):
# The last chunk of the name is a quantisation for most of the list
# and part of the model's name here.
self.assertEqual(ggml.whisper_family("ggml-large-v3-turbo-q8_0.bin"),
"large-v3-turbo")
def test_the_turbo_files_are_not_scattered_through_the_medium_ones(self):
# Sorted by size alone, large-v3-turbo-q5_0 lands between the two
# medium quantisations, half a screen from the model it is a copy of.
models = [listed("ggml-medium-q5_0.bin", 539 << 20),
listed("ggml-large-v3-turbo-q5_0.bin", 574 << 20),
listed("ggml-medium-q8_0.bin", 823 << 20),
listed("ggml-large-v3-turbo.bin", 1624 << 20)]
groups = dict(ggml.whisper_groups(models))
self.assertEqual([i.name for i in groups["large-v3-turbo"]],
["ggml-large-v3-turbo-q5_0.bin",
"ggml-large-v3-turbo.bin"])
self.assertEqual([i.name for i in groups["medium"]],
["ggml-medium-q5_0.bin", "ggml-medium-q8_0.bin"])
def test_the_smallest_model_comes_first_and_the_english_ones_last(self):
models = [listed("ggml-small.en-q5_1.bin", 190 << 20),
listed("ggml-small-q5_1.bin", 190 << 20),
listed("ggml-tiny.bin", 77 << 20)]
groups = ggml.whisper_groups(models)
self.assertEqual([family for family, _ in groups], ["tiny", "small"])
self.assertEqual([i.name for _, group in groups for i in group],
["ggml-tiny.bin", "ggml-small-q5_1.bin",
"ggml-small.en-q5_1.bin"])
+184 -1
View File
@@ -1236,6 +1236,25 @@ class LocalModels(DikteTest):
def _item(name, size=1 << 20):
return hub.Item(name, f"https://example.invalid/{name}", size, "")
@staticmethod
def _rows(box):
"""Every row's text, headings included."""
return [box.model.itemText(row) for row in range(box.model.count())]
@staticmethod
def _repos(box):
return [box.repo.itemText(row) for row in range(box.repo.count())]
@staticmethod
def _offered(box):
"""The model names in the box, headings and duplicates left out."""
names = []
for row in range(box.model.count()):
name = box.model.itemData(row)
if name and name not in names:
names.append(name)
return names
def test_a_row_with_nothing_to_fetch_does_not_offer_a_download(self):
# The model the settings name is not in the list any more, so its row
# was rebuilt from the name alone and carries no file to fetch. The
@@ -1272,7 +1291,7 @@ class LocalModels(DikteTest):
box._on_listed([("models", [self._item("SmolLM3-Q4_K_M.gguf")],
"ggml-org/SmolLM3-3B-GGUF")], "")
self.assertEqual(box.selected(), "SmolLM3-Q4_K_M.gguf")
self.assertEqual(box.model.count(), 1)
self.assertEqual(self._offered(box), ["SmolLM3-Q4_K_M.gguf"])
def test_a_list_for_a_publisher_that_is_no_longer_chosen_is_dropped(self):
# Every change starts its own request, and they do not come back in the
@@ -1300,6 +1319,170 @@ class LocalModels(DikteTest):
time.sleep(0.05)
_app.processEvents()
self.assertEqual(fetch.call_count, 1)
def test_the_models_are_grouped_by_the_model_rather_than_by_size(self):
# Sorted by size alone, the turbo files land between the two medium
# ones, half a screen from the model they are a copy of.
box = self.window(cfg.Config()).local_whisper
with mock.patch.object(ggml, "total_memory", return_value=8 << 30), \
mock.patch.object(ggml, "accelerator", return_value=""):
box._on_listed([("models", [
self._item("ggml-medium-q5_0.bin", 539 << 20),
self._item("ggml-large-v3-turbo-q5_0.bin", 574 << 20),
self._item("ggml-medium-q8_0.bin", 823 << 20),
self._item("ggml-large-v3-turbo.bin", 1624 << 20),
], "")], "")
rows = self._rows(box)
# The two medium files under one heading, the two turbo ones under
# theirs, and the model rather than the file deciding the order.
self.assertEqual(rows[rows.index("medium"):],
["medium",
"ggml-medium-q5_0.bin (539.0 MB, 5-bit)",
"ggml-medium-q8_0.bin (823.0 MB, 8-bit)",
"large-v3-turbo",
"ggml-large-v3-turbo-q5_0.bin "
"(574.0 MB, 5-bit, recommended)",
"ggml-large-v3-turbo.bin (1.6 GB, 16-bit)"])
# A heading is not a model, and nothing can be saved from one.
self.assertIsNone(box.model.itemData(rows.index("medium")))
def test_the_row_for_this_machine_is_on_top_and_says_so(self):
box = self.window(cfg.Config()).local_whisper
with mock.patch.object(ggml, "total_memory", return_value=8 << 30), \
mock.patch.object(ggml, "accelerator", return_value=""):
box._on_listed([("models", [
self._item("ggml-tiny.bin", 77 << 20),
self._item("ggml-large-v3-turbo-q5_0.bin", 574 << 20),
], "")], "")
self.assertEqual(box.selected(), "ggml-large-v3-turbo-q5_0.bin")
self.assertEqual(box.model.itemData(1), "ggml-large-v3-turbo-q5_0.bin")
self.assertIn(t("recommended"), box.model.itemText(1))
def test_a_model_the_memory_cannot_hold_says_so_on_its_row(self):
box = self.window(cfg.Config()).local_llm
box.repo.blockSignals(True)
box.repo.setCurrentText("ggml-org/x-GGUF")
box.repo.blockSignals(False)
with mock.patch.object(ggml, "total_memory", return_value=8 << 30):
box._on_listed([("models", [
self._item("small-Q4_0.gguf", 1 << 30),
self._item("huge-Q8_0.gguf", 12 << 30),
], "ggml-org/x-GGUF")], "")
rows = {box.model.itemData(row): box.model.itemText(row)
for row in range(box.model.count())}
self.assertNotIn(t("too big for this machine"), rows["small-Q4_0.gguf"])
self.assertIn(t("too big for this machine"), rows["huge-Q8_0.gguf"])
def test_a_recommended_row_is_not_listed_twice_after_a_download(self):
# It has a row of its own on top as well as one in its group, and
# reading the rows back the way a finished download does was doubling
# it in the list every time.
box = self.window(cfg.Config()).local_whisper
box._on_listed([("models", [
self._item("ggml-tiny.bin", 77 << 20),
self._item("ggml-large-v3-turbo-q5_0.bin", 574 << 20),
], "")], "")
before = self._offered(box)
box._fill_models_from_current()
self.assertEqual(self._offered(box), before)
names = [box.model.itemData(row) for row in range(box.model.count())]
self.assertEqual(len([n for n in names if n]), len(before) + 1)
def test_a_processor_build_is_not_recommended_the_accurate_model(self):
# The Vulkan loader is on the machine but what was installed is the
# processor build, so there is no card in play whatever the loader
# says, and a 1 GB model on a processor is a wait somebody is sitting
# through with a sentence half typed.
binary = self.path("bin/whisper/v1.9.3/whisper-server")
binary.parent.mkdir(parents=True)
binary.write_text("")
binary.chmod(0o755)
self.path("bin/whisper/installed.json").write_text(json.dumps(
{"tag": "v1.9.3", "binary": str(binary), "backend": "processor"}))
self.patch_attr(ggml.shutil, "which", lambda name: None)
box = self.window(cfg.Config()).local_whisper
with mock.patch.object(ggml, "total_memory", return_value=32 << 30), \
mock.patch.object(ggml, "accelerator", return_value="Vulkan"):
self.assertEqual(box._suggested(), ggml.SUGGESTED_WHISPER)
def test_a_publisher_with_nothing_to_offer_says_why(self):
# Half of what ggml-org publishes is split across files or past the
# size cap, and an empty box read as though the click had not landed.
box = self.window(cfg.Config()).local_llm
box.repo.blockSignals(True)
box.repo.setCurrentText("ggml-org/gpt-oss-120b-GGUF")
box.repo.blockSignals(False)
box._on_listed([("models", [], "ggml-org/gpt-oss-120b-GGUF")], "")
self.assertIn("ggml-org/gpt-oss-120b-GGUF", box.status.text())
self.assertIn("publisher", box.status.text())
def test_an_empty_box_nobody_has_asked_yet_is_not_a_publisher_fault(self):
box = self.window(cfg.Config()).local_llm
box.load("", "ggml-org/SmolLM3-3B-GGUF")
self.assertNotIn("publisher", box.status.text())
def test_only_the_suggested_publishers_are_offered_to_start_with(self):
# Forty repository ids is not a choice anybody can make.
box = self.window(cfg.Config()).local_llm
box._on_listed([("repos", [ggml.SUGGESTED_LLM[0],
"ggml-org/something-else-GGUF"], "")], "")
self.assertEqual(self._repos(box), list(ggml.SUGGESTED_LLM))
def test_a_suggestion_missing_from_the_listing_is_still_offered(self):
# The listing is the forty repositories touched most recently, and a
# publisher that has not been updated in a season falls off it while
# still being the one to point at.
box = self.window(cfg.Config()).local_llm
box._on_listed([("repos", ["ggml-org/something-else-GGUF"], "")], "")
self.assertIn(ggml.SUGGESTED_LLM[0], self._repos(box))
def test_the_switch_brings_the_rest_and_keeps_them_apart(self):
box = self.window(cfg.Config()).local_llm
box._on_listed([("repos", [ggml.SUGGESTED_LLM[0],
"ggml-org/something-else-GGUF"], "")], "")
box.every_repo.setChecked(True)
rows = self._repos(box)
self.assertEqual(rows[:len(ggml.SUGGESTED_LLM)],
list(ggml.SUGGESTED_LLM))
# A separator rather than a heading: the box is typed into as well as
# chosen from, and a heading would land in the field as a repository.
self.assertEqual(rows[len(ggml.SUGGESTED_LLM)], "")
self.assertEqual(rows[-1], "ggml-org/something-else-GGUF")
def test_a_publisher_typed_in_is_not_dropped_by_the_next_fetch(self):
box = self.window(cfg.Config()).local_llm
box.repo.blockSignals(True)
box.repo.setCurrentText("ggml-org/something-else-GGUF")
box.repo.blockSignals(False)
box._on_listed([("repos", [ggml.SUGGESTED_LLM[0],
"ggml-org/something-else-GGUF"], "")], "")
self.assertFalse(box.every_repo.isChecked())
self.assertIn("ggml-org/something-else-GGUF", self._repos(box))
self.assertEqual(box.repository(), "ggml-org/something-else-GGUF")
def test_the_chosen_publisher_is_said_in_words(self):
# A repository id names the publisher, the parameter count and the
# shape of the weights, and none of that says whether to click it.
box = self.window(cfg.Config()).local_llm
box.repo.setCurrentText(ggml.SUGGESTED_LLM[0])
self.assertTrue(box.repo_note.text())
box.repo.setCurrentText("ggml-org/nobody-wrote-a-note-GGUF")
self.assertEqual(box.repo_note.text(), "")
def test_the_box_says_what_this_machine_will_run_on(self):
box = self.window(cfg.Config()).local_whisper
with mock.patch.object(ggml, "accelerator", return_value="Vulkan"), \
mock.patch.object(ggml, "total_memory", return_value=32 << 30):
box._show_machine()
self.assertIn("Vulkan", box.machine_label.text())
self.assertIn("32.0 GB", box.machine_label.text())
def test_a_machine_with_no_card_is_told_it_is_on_the_processor(self):
box = self.window(cfg.Config()).local_whisper
with mock.patch.object(ggml, "accelerator", return_value=""), \
mock.patch.object(ggml, "total_memory", return_value=8 << 30):
box._show_machine()
self.assertIn("processor", box.machine_label.text())
def test_a_processor_build_where_the_vulkan_one_belongs_says_so(self):
# The Vulkan whisper-server is published by hand, and until it is
# there the download lands upstream's processor build. Said nowhere,