Do not let a shrugging sysconf turn a workstation into a tiny machine

Three from the review of the change before this one.

sysconf answers -1 for a limit it holds to be indeterminate, and CPython
hands that back rather than raising, so the page count times the page size
came out negative. A negative is truthy, so it went past the check for a
machine nothing could be read from and floored at half a gigabyte: a 64 GB
workstation was told every model past 512 MB was too big for it, the
suggestion dropped to small-q5_1, and the machine line read "Memory:
-4096 B". Anything not positive is now the unknown machine it always was.

The memory is read once and kept. It does not change while Dikte runs, and
a thirty row list asked seventy times per draw, which on the Mac path is
seventy processes started on the interface thread every time a download
finished, a model was deleted or a publisher changed.

And "test-" is matched as a plain substring, so it was also inside
"Latest-" and dropped a publisher nothing is wrong with. Anchored the way
every other mark in that list already is.
This commit is contained in:
2026-09-05 11:22:12 +03:00
parent 08fc2e4a9d
commit 74e17cbd15
3 changed files with 59 additions and 1 deletions
+28 -1
View File
@@ -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")
+5
View File
@@ -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.
+26
View File
@@ -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"):