diff --git a/dikte/ggml.py b/dikte/ggml.py index da17dde..dbf768f 100644 --- a/dikte/ggml.py +++ b/dikte/ggml.py @@ -130,8 +130,11 @@ GGUF_MAX_BYTES = 16 << 30 # on: a vision or audio tower with no text half worth running, a speech model, # and the base models, which continue text rather than following an instruction # and answer a cleanup prompt by carrying on writing the transcript. +# Matched as plain substrings, so every one of these carries its own +# delimiters: an unanchored "test-" is also inside "Latest-" and would drop a +# publisher that is perfectly usable. LLM_REPO_SKIP = ("-Base-GGUF", "-VL-", "-Vision-", "-Omni-", "-Video-", - "-TTS-", "parakeet", "test-") + "-TTS-", "parakeet", "/test-") GB = 1 << 30 @@ -219,6 +222,8 @@ MEMORY_OVERHEAD = GB # anything. Enough for the smallest whisper models and for a sub-billion # cleanup model, which is what such a machine can run. MEMORY_FLOOR = GB // 2 +# What total_memory() read the one time it asked. None until it has. +_MEMORY = None class LocalError(Exception): @@ -713,6 +718,28 @@ def total_memory(): Zero is a real answer and not a failure: every caller treats an unknown machine as one big enough for whatever it is looking at, because a wrong "too big" is worse advice than none. + + Read once and kept. The memory in a machine does not change while Dikte + runs, and a list of thirty rows asks this question seventy times: on the + Mac path below, where the answer comes from a program rather than a + library call, that was seventy processes started on the interface thread + every time a list was drawn. + """ + global _MEMORY + if _MEMORY is None: + _MEMORY = max(_read_memory(), 0) + return _MEMORY + + +def _read_memory(): + """What the system says, which on a bad day is a negative number. + + sysconf answers -1 for a limit it holds to be indeterminate, and CPython + hands that straight back rather than raising, so the product below can + come out negative. The caller floors it at zero, which is the answer for + a machine nothing could be read from: a 64 GB workstation whose sysconf + shrugged was otherwise being told every model past 512 MB was too big + for it. """ try: return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") diff --git a/tests/support.py b/tests/support.py index 8a005f1..f96fab9 100644 --- a/tests/support.py +++ b/tests/support.py @@ -24,6 +24,7 @@ from unittest import mock from dikte import assistant from dikte import config as cfg +from dikte import ggml from dikte import i18n from dikte import update @@ -95,6 +96,10 @@ class DikteTest(unittest.TestCase): i18n.set_language("en") self.addCleanup(i18n.set_language, "en") + # Read once and kept for the life of the process, which across a test + # run means one test's machine answering for the next one's. + self.patch_attr(ggml, "_MEMORY", None) + # cli.launch_gui replaces this process with the application when no # instance is running. A test that reaches it would take the whole run # with it and hang, so it fails loudly here instead. diff --git a/tests/test_ggml.py b/tests/test_ggml.py index 9a1897b..bff3f61 100644 --- a/tests/test_ggml.py +++ b/tests/test_ggml.py @@ -696,6 +696,12 @@ class Catalogue(Local): self.assertNotIn("ggml-org/parakeet-GGUF", found) self.assertNotIn("ggml-org/Qwen3-8B-Base-GGUF", found) + def test_a_publisher_is_not_dropped_for_a_word_it_happens_to_contain(self): + # The skip marks are matched as plain substrings, and an unanchored + # "test-" is also inside "Latest-". + self.assertTrue(ggml.can_clean("ggml-org/Qwen3-Latest-GGUF")) + self.assertFalse(ggml.can_clean("ggml-org/test-model-router-download")) + 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 @@ -1252,6 +1258,26 @@ class Machine(Local): mock.patch.object(ggml.subprocess, "run", answer): self.assertEqual(ggml.total_memory(), 32 * ggml.GB) + def test_a_sysconf_that_shrugs_is_an_unknown_machine_and_not_a_tiny_one(self): + # sysconf answers -1 for a limit it holds to be indeterminate and + # CPython hands that back rather than raising, so the product came out + # negative: a 64 GB workstation was told every model past 512 MB was + # too big for it, and the machine line read "Memory: -4096 B". + with mock.patch.object(ggml.os, "sysconf", lambda name: + 4096 if name == "SC_PAGE_SIZE" else -1): + self.assertEqual(ggml.total_memory(), 0) + self.assertTrue(ggml.fits(574 << 20, memory=0)) + + def test_the_memory_is_read_once_and_kept(self): + # A list of thirty rows asks seventy times, and on the Mac path the + # answer comes from a program rather than a library call. + calls = [] + with mock.patch.object(ggml, "_read_memory", + lambda: calls.append(1) or 16 * ggml.GB): + self.assertEqual(ggml.total_memory(), 16 * ggml.GB) + self.assertEqual(ggml.total_memory(), 16 * ggml.GB) + self.assertEqual(len(calls), 1) + 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"):