Pictures on by default on every make of card: MCP install, Qwen3.6, Unsloth and the original Flash-Next families follow the one reader rule (CPU under 24 GB, GPU from 24 GB); the CPU reader keeps no VRAM reserve

This commit is contained in:
Niko1221 committed 2026-10-19 07:23:44 +02:00
1 parent fd0e3766a3
commit 91baead754
11 files changed
+179 -82

No files matched your search

+1 -1
View File
@@ -128,7 +128,7 @@ Every result is JSON with a one-line `summary`. It is sent both as text and as M
- the size setup itself would pick: IQ3_XXS from 60 GB of RAM, Q2_0 from about 44 GB, the Coder from about 28 GB,
and the Coder in the low-RAM mode below that when the GPU makes up for the RAM;
- the context setup recommends for the GPU's VRAM;
- images off.
- pictures on where the model can read them (the reader runs on the CPU under 24 GB of VRAM and on the GPU from 24 GB, on every make of card; a Mac has none yet).
The first call returns the plan: the download size (58-111 GB), the free disk space, the RAM check, and the exact
setup command. The assistant should show you the plan and call again with `confirm: true` once you agree.
+32 -36
View File
@@ -571,7 +571,7 @@ ESP_VECTOR = ROOT / "data" / "experimental-speed-projection" / "Qwen3.8-Flash-Ne
# sizes its expert slots around it and the default reserve (700 MiB) is enough; engines before 0.1.2 need more
VISION_GPU_SMALL_RESERVE_MIB = 1000 # the tip for images on a <= 12 GB card (the engine's LOW line asked ~1003)
VISION = {"gpu": {"max_tokens": 1024, "reserve_mib": 700},
"cpu": {"max_tokens": 300, "reserve_mib": 700}}
"cpu": {"max_tokens": 300, "reserve_mib": 0}} # the CPU reader takes no VRAM: no reserve (serve/server.py adds 700 only for a GPU reader)
EXE = "strata.exe" if WIN else "strata"
VEXE = "strata-vision.exe" if WIN else "strata-vision"
HVEXE = "strata-vision-hip.exe" if WIN else "strata-vision-hip" # AMD: the picture reader on the GPU (tools/vision, STRATA_VISION_HIP)
@@ -2127,20 +2127,15 @@ def unsloth_budget(model, ram, gpu=None, asked=None) -> bool:
ram < MODELS[model]["arena_gb"] / 1.073741824 + UNSLOTH_RESIDENT_BESIDE_GB
UNSLOTH_IMAGES_MIN_VRAM_GB = 15.5 # a 16 GB card: the GPU encoder's ~1.4 GB is under a tenth of it
def images_default(model, family, ram, gpu=None) -> bool:
"""Is "images on" setup's default answer (the question's Enter, and --yes) for this choice? Owner rule 2026-10-18: pictures are on
wherever the model can read them - the original Flash-Next, Swift and Coder files, Unsloth's, NVFP4 - on every make of card; which
reader runs is strata_control/vision_reader.py's rule (the CPU under 24 GB of VRAM, the GPU from 24 GB). The CPU reader takes no
VRAM (no --vram-reserve-mib), so a small card is no exception."""
return MODELS.get(model, {}).get("vision", FAMILIES.get(family, {}).get("vision")) is not False
def unsloth_images_default(model, family, ram, gpu=None) -> bool:
"""MODEL-UPDATE: is "images on" setup's default answer for this choice: Unsloth's files (the same base model and
image encoder as ISTA's, run end to end on CUDA and on HIP), on an NVIDIA card of 16 GB or more (the GPU encoder
keeps ~1.4 GB free) or, on AMD, wherever every expert stays in RAM (the CPU encoder takes no VRAM). Smaller cards
keep "no" as the default (asked, as for the other families)."""
if family != "unsloth" or not MODELS.get(model, {}).get("budget") or MODELS[model].get("vision") is False:
return False
g = gpu or {}
if str(g.get("arch", "")).startswith("gfx"):
return not unsloth_budget(model, ram, g) and not WIN
return g.get("vram_gb", 0.0) >= UNSLOTH_IMAGES_MIN_VRAM_GB
unsloth_images_default = images_default # the earlier name
IMAGES_GPU_ENCODER_MIN_VRAM_GB = 23.5 # = strata_control/vision_reader.py GPU_MIN_VRAM_GB, the one place for the rule; #2406: below this the images answer "yes" gets the CPU encoder (a card of 12-16 GB: the GPU
@@ -6075,33 +6070,32 @@ def own_path_size(fam: dict, ram: float) -> str:
def own_path_vision(a, fam: dict, gpu: dict, hip: bool) -> str:
"""The image encoder for an own-path family (Qwen3.6-35B-A3B): "gpu", "cpu" or "none". The Flash-Next families' rules:
--vision yes|no|gpu|cpu, else a question whose default answer is yes where the GPU encoder costs little - an NVIDIA
card of 16 GB or more (its ~1.4 GB is under a tenth of it) - and, on AMD under Linux, yes with the CPU encoder (it takes
no VRAM; there is no GPU encoder for AMD yet). A smaller NVIDIA card is recommended the CPU encoder (the GPU one takes
room from the experts); what is asked for with --vision gpu is kept, with a note (recommend, never force)."""
"""The image encoder for an own-path family (Qwen3.6-35B-A3B): "gpu", "cpu" or "none". Pictures are on by default (owner rule
2026-10-18) and the reader follows strata_control/vision_reader.py's one rule on every make of card: the CPU encoder under 24 GB of
VRAM (the GPU one takes ~1.4 GB the experts would use), the GPU encoder from 24 GB. --vision yes|no|gpu|cpu, else a question whose
default answer is yes; what is asked for with --vision gpu / cpu is kept, with a note (recommend, never force)."""
asked = a.vision
vram = float(gpu.get("vram_gb", 0.0))
big = vram >= UNSLOTH_IMAGES_MIN_VRAM_GB
if asked in ("no", "none"):
return "none"
if asked is None:
from strata_control import vision_reader as VR
mode = VR.default_mode(vram)
say()
say(f" Images: {fam['title']} can also read pictures (screenshots, photos, scanned pages). This adds a "
f"{fam['sha256'][fam['mmproj']][0] / 1e9:.1f} GB download" + (
"; the encoder runs on the GPU when the engine has the AMD one (about 1 GB of VRAM), else on the CPU." if hip else
" and the image encoder runs on the GPU (~1.4 GB of VRAM kept free, 0.1-0.5 s per picture)." if big else
" and the image encoder runs on the GPU (about 1-1.4 GB of VRAM kept free, a fraction of a second per picture)."
if mode == "gpu" else
"; the encoder runs on the CPU (a few seconds per picture), because the GPU encoder would take ~1.4 GB "
"of VRAM from the experts on this card."))
default = "y" if (big or hip) else "n"
if ask("Do you want images?", ["y", "n"], default, a.yes) != "y":
"of VRAM from the experts on this card (--vision gpu to use it anyway)."))
if ask("Do you want images?", ["y", "n"], "y", a.yes) != "y":
return "none"
asked = "yes" if hip else "cpu" if not big else "gpu"
asked = "yes"
if hip:
return hip_vision(asked, vram) # settled against the engine's readers in hip_reader() (GPU reader, else CPU)
if asked == "yes":
asked = "gpu" if big else "cpu"
if asked == "gpu" and not big and vram > 0:
asked = "gpu" if (vram <= 0 or vram >= IMAGES_GPU_ENCODER_MIN_VRAM_GB) else "cpu"
if asked == "gpu" and 0 < vram < IMAGES_GPU_ENCODER_MIN_VRAM_GB:
warn(f"the GPU image encoder takes ~1.4 GB of VRAM, which on a {vram:.0f} GB card is room the experts would use: "
"--vision cpu reads pictures on the CPU instead (a few seconds each); kept as you chose")
return asked
@@ -6573,7 +6567,8 @@ def main() -> int:
help="KV cache precision above 8K context: int8 (default), q4_0 (half the memory, a little less "
"precise) or k8v4 (hybrid: INT8 K + 4-bit V, 816 B/cell)")
ap.add_argument("--vision", choices=["yes", "no", "none", "gpu", "cpu"],
help="let the model read images (yes = the encoder on the GPU)")
help="let the model read images (on by default where the model can; yes = the best reader for this card: the CPU "
"under 24 GB of VRAM, the GPU from 24 GB, any make; gpu / cpu = that reader; no = text only)")
ap.add_argument("--vision-tokens", type=int, metavar="N",
help="the most image tokens a picture becomes (default 1024 with the encoder on the GPU, 300 on "
"the CPU): more reads small text and charts better, and takes longer to encode; remembered "
@@ -6814,7 +6809,7 @@ def main() -> int:
+ " (recommended)")
say(" 2) AMD: " + ", ".join(f"{g['name']} ({g['vram_gb']:.0f} GB{', unified memory' if g.get('uma') else ''})"
for g in amd_ok)
+ f" ({'the ready-made AMD engine, no images' if WIN else 'compiled here, images on the CPU'}"
+ f" ({'the ready-made AMD engine' if WIN else 'compiled here'}, pictures on"
" - docs/AMD_HIP.md)")
say(" One engine drives the cards of one make: a model is never split across NVIDIA and AMD cards, and two models at the same")
say(" time, one on each, is not supported yet.")
@@ -7182,10 +7177,9 @@ def main() -> int:
if a.vision not in (None, "no", "none"):
warn(f"images are not available with {model} yet: off")
elif hip:
# MODEL-UPDATE: Unsloth's files on AMD (Linux) read images with the CPU encoder by default where their experts
# all stay in RAM (it takes no VRAM; measured on an R9700 with UD-Q4_K_XL and UD-Q5_K_XL, docs/QUANTS.md)
vision = hip_vision(a.vision if a.vision is not None or not unsloth_images_default(model, family, ram, gpu)
else "cpu", float((gpu or {}).get("vram_gb") or 0.0))
# Unsloth's files on AMD: pictures on by default, the reader by the 24 GB rule (hip_vision; docs/QUANTS.md)
vision = hip_vision(a.vision if a.vision is not None or not images_default(model, family, ram, gpu)
else "yes", float((gpu or {}).get("vram_gb") or 0.0))
elif a.vision:
vision = flash_vision_encoder(a.vision, gpu) if a.vision != "no" else "none"
vision = "none" if vision in ("no", "none") else vision
@@ -7200,8 +7194,8 @@ def main() -> int:
say(" more on some PCs. --vision gpu if you want pictures in under a second.")
else:
say(" download and keeps ~1.4 GB of VRAM free for the image encoder, so text is a few % slower.")
# MODEL-UPDATE: on by default for Unsloth's files on a card with room for the encoder (16 GB and more)
default = "y" if unsloth_images_default(model, family, ram, gpu) else "n"
# on by default wherever the model can read pictures (the reader by the 24 GB rule)
default = "y" if images_default(model, family, ram, gpu) else "n"
vision = flash_vision_encoder("yes", gpu) if ask("Do you want images?", ["y", "n"], default, a.yes) == "y" else "none"
ok("images: " + {"none": "off", "gpu": "on", "cpu": "on (encoder on the CPU)"}[vision])
if vision != "none" and MODELS[model].get("vision_untested"):
@@ -7581,7 +7575,9 @@ def main() -> int:
"cache: it stays on auto, which may take the room the OS needs (#1715). A smaller "
"--resident-budget-gib leaves room")
if vision != "none":
args += ["--vision", "--vram-reserve-mib", str(VISION[vision]["reserve_mib"])]
args += ["--vision"]
if VISION[vision]["reserve_mib"]:
args += ["--vram-reserve-mib", str(VISION[vision]["reserve_mib"])]
if vision == "gpu" and a.vram_reserve_mib is None and 0 < gpu.get("vram_gb", 0.0) <= 12.5:
# a tip only (recommend, never force): on a 12 GB card the encoder's 700 MiB can leave ~200 MiB free
print(f" tip: images on a {gpu['vram_gb']:.0f} GB card can leave little VRAM free; if a request stalls, "
+1 -1
View File
@@ -450,7 +450,7 @@ class Runner:
def vision_word(self) -> str:
"""What setup is told for the picture choice. "on" is setup's "yes": setup (Control mode) picks the CPU encoder on a card under 24 GB
and on AMD, the GPU encoder on a bigger NVIDIA card, and downloads the pinned picture-reader file (mmproj) with the model."""
and the GPU encoder from 24 GB (any make of card), and downloads the pinned picture-reader file (mmproj) with the model."""
s = self.s
self.resolved_vision()
if s.vision == "off":
+11 -14
View File
@@ -1044,7 +1044,7 @@ class Strata:
notes.append("this CPU has no AVX2: EXPERIMENTAL and slow - setup compiles the engine on this PC for the "
"older CPU (10-20 minutes), and the CPU's share of the experts runs a few times slower")
if backend == "hip":
notes.append("AMD (experimental): no GPU picture reader yet")
notes.append("AMD (experimental): pictures are on; the reader runs on the CPU under 24 GB of VRAM and on the GPU from 24 GB")
cmd = (("START-HERE.bat --setup" if WIN else "./setup.sh --setup") +
f" --yes --family {fam} --model {model} --context {ctx}" + (" --backend hip" if backend == "hip" else ""))
return {"family": fam, "model": model, "title": families[fam]["title"] + " " + model, "context": ctx,
@@ -1491,7 +1491,7 @@ def tool_defs(models: dict, families: dict, contexts: list) -> list:
"script). Without confirm=true it only returns the plan (what would be downloaded, disk "
"space): show it to the user and call again with confirm=true once they agree. Returns at "
"once; call it again (no arguments needed) to see the progress, or with cancel=true to stop "
"it (it resumes later). Defaults: the recommended model and context for this PC, images off. "
"it (it resumes later). Defaults: the recommended model and context for this PC, pictures on where the model can read them. "
"After it finishes, start the model with strata_start.",
"inputSchema": {"type": "object", "additionalProperties": False, "properties": {
"family": {"type": "string", "enum": list(families),
@@ -1505,8 +1505,10 @@ def tool_defs(models: dict, families: dict, contexts: list) -> list:
"context": {"type": "integer", "enum": contexts,
"description": "context length in tokens (more needs more VRAM)"},
"vision": {"type": "string", "enum": ["no", "yes", "gpu", "cpu"],
"description": "let the model read images (yes = the encoder on the GPU; ~0.9 GB more "
"download); default no"},
"description": "let the model read images (~0.9 GB more download). Default yes where the model can "
"read them (not on a Mac): yes = the best reader for this card (the CPU "
"under 24 GB of VRAM, the GPU from 24 GB, any make), gpu / cpu = that "
"reader, no = text only"},
"kv": {"type": "string", "enum": ["int8", "q4_0", "k8v4"],
"description": "KV cache precision above 8K context (default int8)"},
"gpu": {"type": "integer", "minimum": 0, "maximum": 63,
@@ -1824,16 +1826,11 @@ class Tools:
context = 8192 if vram < 14 else 32768
if vision in ("yes", "gpu", "cpu") and md.get("vision", families[family].get("vision")) is False:
raise ToolError(f"images are not available with {families[family]['title']} {model} yet: use vision=no")
if vision is not None and backend == "hip" and vision != "no":
# setup.py's hip_vision rules (#990): the AMD backend has no GPU image encoder, but --vision cpu reads
# images on the CPU beside it (Linux; the ready-made Windows AMD engine has no encoder)
if vision == "cpu" and not WIN:
pass
elif vision == "cpu":
raise ToolError("images on the CPU with an AMD card are Linux-only for now: use vision=no")
else:
raise ToolError("the AMD backend has no GPU image encoder yet: use vision=cpu (images on the CPU, "
"Linux) or vision=no")
if vision is None:
# owner rule (2026-10-18): pictures are on by default wherever the model can read them and the engine has a reader; setup picks
# the reader (strata_control/vision_reader.py: the CPU under 24 GB, the GPU from 24 GB, every make of card). A Mac has none yet.
can_read = md.get("vision", families[family].get("vision")) is not False
vision = "yes" if can_read and not mac else "no"
target = self.check_data_dir(data_dir) if data_dir else s.data_dir()
tag = (families[family].get("tag", "") + model)
have_dir = target / "models" / tag
+76
View File
@@ -0,0 +1,76 @@
"""Owner rule 2026-10-18: pictures are on by default wherever the model and the engine package can read them; the reader runs on the
CPU under 24 GB of VRAM and on the GPU from 24 GB, on every make of card. Checks setup.images_default and the shared rule.
python -m unittest tools.test_picture_defaults
"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import setup # noqa: E402
from strata_control import vision_reader as VR # noqa: E402
class PictureDefaults(unittest.TestCase):
def test_unsloth_default_is_on_for_every_make_and_size(self):
models = [m for m, d in setup.MODELS.items() if d.get("budget") and d.get("vision") is not False]
self.assertTrue(models)
for m in models:
for gpu in ({"vram_gb": 10.0}, {"vram_gb": 12.0}, {"vram_gb": 16.0}, {"vram_gb": 32.0},
{"vram_gb": 16.0, "arch": "gfx1201"}, {"vram_gb": 24.0, "arch": "gfx1100"}, {}):
for win in (False, True):
old = setup.WIN
setup.WIN = win
try:
self.assertTrue(setup.images_default(m, "unsloth", 64.0, gpu), (m, gpu, win))
self.assertTrue(setup.images_default(m, "unsloth", 512.0, gpu), (m, gpu, win))
finally:
setup.WIN = old
def test_yes_means_pictures_on_for_qwen_swift_and_coder(self):
"""setup --yes (and Enter at the question) on the original Flash-Next, Swift and Coder files: pictures on, the reader by the 24 GB rule,
and no VRAM reserve for the CPU reader."""
from test_setup_golden import PROFILES, install
for key, gpu in (("64GB-1x32GB", True), ("96GB-1x16GB", False)): # 31.8 GB: the GPU reader; 15.9 GB: the CPU reader
ram, found = PROFILES[key]
for fam, model in (("qwen", "Q2_0"), ("swift", "IQ3_S"), ("coder", "IQ1_M")):
for answers in (None, ""):
code, out, cfg, _ = install(ram, found, ["--family", fam, "--model", model, "--no-start"], answers=answers)
where = (key, fam, model, answers)
self.assertEqual(code, 0, (where, out[-1500:]))
self.assertIn("--vision", cfg["args"], where)
self.assertEqual(cfg["vision"]["gpu"], gpu, where)
self.assertEqual("--vram-reserve-mib" in cfg["args"], gpu, where) # only the GPU reader keeps VRAM free
self.assertIn("images: on", out, where)
def test_pictures_off_stays_available(self):
from test_setup_golden import PROFILES, install
ram, found = PROFILES["64GB-1x32GB"]
for fam, model in (("qwen", "Q2_0"), ("swift", "IQ3_S"), ("coder", "IQ1_M")):
code, out, cfg, _ = install(ram, found, ["--family", fam, "--model", model, "--no-start", "--vision", "no"])
self.assertEqual(code, 0, out[-1500:])
self.assertNotIn("--vision", cfg["args"])
self.assertNotIn("vision", cfg)
def test_the_cpu_reader_takes_no_vram_reserve(self):
"""So a card of 8 GB is no exception: the reserve is only for the GPU reader."""
self.assertEqual(setup.VISION["cpu"]["reserve_mib"], 0)
self.assertEqual(setup.VISION["gpu"]["reserve_mib"], 700)
def test_one_rule_for_the_reader(self):
self.assertEqual(setup.IMAGES_GPU_ENCODER_MIN_VRAM_GB, VR.GPU_MIN_VRAM_GB)
for vram, want in ((8, "cpu"), (16, "cpu"), (23.0, "cpu"), (24, "gpu"), (32, "gpu"), (None, "gpu")):
self.assertEqual(VR.default_mode(vram), want)
self.assertEqual(VR.wanted("yes", vram), want)
self.assertEqual(setup.hip_vision("yes", vram or 0.0), want if vram else "gpu")
self.assertEqual(setup.hip_vision(None, 16.0), "cpu")
self.assertEqual(setup.hip_vision("gpu", 16.0), "gpu")
if __name__ == "__main__":
unittest.main()
+5 -1
View File
@@ -443,8 +443,12 @@ class VramReserve(unittest.TestCase):
self.assertEqual(a.count("--vram-reserve-mib"), 1)
self.assertEqual(a[a.index("--vram-reserve-mib") + 1], "2048")
self.assertIn("VRAM kept free for other programs: 2048 MiB", out)
code, out, cfg, _ = self.install()
code, out, cfg, _ = self.install("--vision", "no")
self.assertNotIn("--vram-reserve-mib", cfg["args"]) # not given: the engine's default, as before
code, out, cfg, _ = self.install("--vision", "cpu")
self.assertNotIn("--vram-reserve-mib", cfg["args"]) # the CPU reader needs no reserve
code, out, cfg, _ = self.install()
self.assertEqual(cfg["args"][cfg["args"].index("--vram-reserve-mib") + 1], "700") # pictures on by default, GPU reader on a 32 GB card: its 700 MiB
def test_with_images_one_value(self):
code, out, cfg, _ = self.install("--vision", "cpu", "--vram-reserve-mib", "1500")
+1 -1
View File
@@ -227,7 +227,7 @@ class MockedInstall(unittest.TestCase):
self.assertTrue(self.flag(a, "--embd-gguf").endswith("/token-embd-bf16.gguf"))
self.assertNotIn("--mmap-experts", a) # 128 GB: every expert in RAM
self.assertNotIn("--resident-experts", a)
self.assertNotIn("vision", cfg) # --yes: images default off, as for every model
self.assertIn("vision", cfg) # --yes: pictures on, as for every model that can read them
self.assertEqual(cfg["model_name"], "qwen3.8-flash-next-nvfp4-nvfp4")
self.assertIn("Qwen Community License", out)
+1
View File
@@ -163,6 +163,7 @@ def fake_prebuilt(calls):
(eng / "BUILD.json").write_text(json.dumps({"version": "0.1.39", "source": "release", "archs": [86], "ptx": True,
"cuda": "12.9" if toolkit == 12 else "13.0", "vision": "gpu"}))
(eng / setup.EXE).write_bytes(b"")
(eng / setup.VEXE).write_bytes(b"") # pictures are on by default: the engine carries the reader
return eng
return get
+16 -5
View File
@@ -190,8 +190,8 @@ class InstallOwnPath(unittest.TestCase):
a = self.cfg["args"]
self.assertEqual(a[a.index("--max-context") + 1], "131072")
def test_images_by_default_on_a_16gb_nvidia_card_use_the_gpu_encoder(self):
rc, out = self.install(model="UD-Q4_K_XL", vram=16.0) # --yes: the question's default answer
def test_images_by_default_on_a_24gb_plus_nvidia_card_use_the_gpu_encoder(self):
rc, out = self.install(model="UD-Q4_K_XL", vram=24.0) # --yes: the question's default answer (yes)
self.assertEqual(rc, 0, out)
self.assertIn(("engine", "gpu"), self.calls)
mm = self.root / "models" / "qwen36-UD-Q4_K_XL" / "mmproj-F16.gguf"
@@ -209,9 +209,12 @@ class InstallOwnPath(unittest.TestCase):
self.assertIn("images: on", out)
def test_a_12gb_card_is_recommended_the_cpu_encoder_never_forced(self):
rc, out = self.install(model="UD-Q4_K_XL", vram=12.0) # the default answer is no
rc, out = self.install(model="UD-Q4_K_XL", vram=12.0) # the default answer is yes, with the CPU encoder
self.assertEqual(rc, 0, out)
self.assertNotIn("vision", self.cfg)
self.assertFalse(self.cfg["vision"]["gpu"])
rc, out = self.install(model="UD-Q4_K_XL", vram=16.0) # the 24 GB rule, not the old 16 GB one
self.assertEqual(rc, 0, out)
self.assertFalse(self.cfg["vision"]["gpu"])
rc, out = self.install(model="UD-Q4_K_XL", vram=12.0, vision="yes") # yes: the CPU encoder, said why
self.assertEqual(rc, 0, out)
self.assertFalse(self.cfg["vision"]["gpu"])
@@ -253,6 +256,14 @@ class InstallOwnPath(unittest.TestCase):
self.assertEqual(rc, 0, out)
self.assertTrue(self.cfg["vision"]["gpu"])
def test_amd_under_24gb_reads_pictures_on_the_cpu_by_default(self):
with mock.patch.object(setup, "WIN", False), mock.patch.object(setup, "VEXE", "strata-vision"), mock.patch.object(setup, "HVEXE", "strata-vision-hip"):
rc, out = self.install(model="UD-Q4_K_XL", vram=16.0, hip=True)
self.assertEqual(rc, 0, out)
self.assertIn("--vision", self.cfg["args"])
self.assertFalse(self.cfg["vision"]["gpu"])
self.assertEqual(Path(self.cfg["vision"]["exe"]).name, "strata-vision")
def test_parallel_is_opt_in_and_recommended_where_the_experts_fit(self):
fam = setup.OWN_PATH_FAMILIES["qwen36"]
# nothing written without --parallel; the recommendation is said
@@ -321,7 +332,7 @@ class InstallOwnPath(unittest.TestCase):
p.write_text(json.dumps(self.cfg), encoding="utf-8")
ch = setup.choices_from_config(p)
self.assertEqual((ch["family"], ch["model"], ch["context"]), ("qwen36", "UD-Q6_K_XL", 65536))
self.assertEqual(ch["vision"], "gpu") # 16 GB NVIDIA, the default answer
self.assertEqual(ch["vision"], "cpu") # 16 GB NVIDIA, the default answer: the CPU encoder under 24 GB
self.assertTrue(setup.own_path_config(self.cfg))
self.assertEqual(setup.calibration_targets([p], None, None), []) # the tuning is Flash-Next's
+12 -8
View File
@@ -221,7 +221,8 @@ class Main(Base):
self.assertEqual(code, 0, out)
self.assertNotIn("EXPERIMENTAL", out)
base = f"https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF/resolve/{REV}/UD-Q4_K_XL/"
self.assertEqual(self.downloads, [base + n for n in setup.UNSLOTH_SHARDS]) # no image encoder
self.assertEqual(self.downloads[:-1], [base + n for n in setup.UNSLOTH_SHARDS]) # then the image encoder: pictures are on by default
self.assertIn("mmproj", self.downloads[-1])
self.assertEqual([v[0] for v in self.verified], list(setup.UNSLOTH_SHARDS))
self.assertEqual([v[1:] for v in self.verified], list(setup.UNSLOTH_SHARDS.values()))
packs = [r for r in self.runs if r[1].endswith("iq_pack.py")]
@@ -232,12 +233,12 @@ class Main(Base):
args = cfg["args"]
self.assertEqual(args[args.index("--resident-budget-gib") + 1], "40")
self.assertTrue(args[args.index("--native") + 1].endswith("-00001-of-00004.gguf"))
for flag in ("--ple-gguf", "--mmap-experts", "--resident-experts", "--vision", "--control-vector-scaled",
for flag in ("--ple-gguf", "--mmap-experts", "--resident-experts", "--control-vector-scaled",
"--kv-resident"):
self.assertNotIn(flag, args)
self.assertEqual(args[args.index("--max-context") + 1], "8192")
self.assertEqual(cfg["model_name"], "qwen3.8-flash-next-unsloth-ud-q4_k_xl")
self.assertNotIn("vision", cfg)
self.assertFalse(cfg["vision"]["gpu"]) # 11.9 GB: the CPU reader
def test_recommended_context_on_12_gb(self):
code, out, cfg = self.main([])
@@ -472,7 +473,8 @@ class IQ4XS(Base):
self.assertEqual(code, 0, out)
self.assertNotIn("is EXPERIMENTAL", out) # (the menu still lists UD-Q4_K_XL)
base = f"https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF/resolve/{REV}/UD-IQ4_XS/"
self.assertEqual(self.downloads, [base + n for n in setup.UNSLOTH_IQ4_XS_SHARDS])
self.assertEqual(self.downloads[:-1], [base + n for n in setup.UNSLOTH_IQ4_XS_SHARDS]) # then the image encoder (pictures on by default)
self.assertIn("mmproj", self.downloads[-1])
self.assertEqual(self.verified, [(n, *v) for n, v in setup.UNSLOTH_IQ4_XS_SHARDS.items()])
packs = [r for r in self.runs if r[1].endswith("iq_pack.py")]
self.assertEqual(len(packs), 1, self.runs)
@@ -482,14 +484,15 @@ class IQ4XS(Base):
args = cfg["args"]
self.assertEqual(args[args.index("--resident-budget-gib") + 1], "40")
self.assertTrue(args[args.index("--native") + 1].endswith("UD-IQ4_XS-00001-of-00003.gguf"))
for flag in ("--ple-gguf", "--mmap-experts", "--resident-experts", "--vision"):
for flag in ("--ple-gguf", "--mmap-experts", "--resident-experts"):
self.assertNotIn(flag, args)
self.assertIn("--vision", args) # pictures are on by default
self.assertEqual(cfg["model_name"], "qwen3.8-flash-next-unsloth-ud-iq4_xs")
pack = self.t / "data" / "packs" / "unsloth-ud-iq4_xs" # its own pack and tokenizer
self.assertEqual(packs[0][packs[0].index("--out") + 1], str(pack))
self.assertEqual(args[args.index("--pack") + 1], str(pack))
self.assertEqual(cfg["tokenizer"], str(pack / "tokenizer"))
self.assertNotIn("vision", cfg) # images: asked, off by default
self.assertFalse(cfg["vision"]["gpu"]) # pictures on by default, the CPU reader under 24 GB
with tempfile.TemporaryDirectory() as t: # a start reads the choice back
p = Path(t) / "strata-unsloth-ud-iq4_xs.json"
p.write_text(json.dumps(cfg))
@@ -824,8 +827,9 @@ class BigFiles(Base):
self.assertEqual(code, 0, out)
base = f"https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF/resolve/{REV}/Q8_0/"
names = [setup.model_file(setup.FAMILIES["unsloth"], Q8, i) for i in range(1, 7)]
self.assertEqual(self.downloads, [base + n for n in names])
self.assertEqual([v[0] for v in self.verified], names)
self.assertEqual(self.downloads[:-1], [base + n for n in names]) # then the image encoder (pictures on by default)
self.assertIn("mmproj", self.downloads[-1])
self.assertEqual([v[0] for v in self.verified][:len(names)], names)
packs = [r for r in self.runs if r[1].endswith("iq_pack.py")]
self.assertIn("--compat-bf16", packs[0])
args = cfg["args"]
+23 -15
View File
@@ -308,22 +308,30 @@ class Validation(FakeRoot):
self.assertEqual(res["plan"]["images"], "yes")
self.assertRejected("strata_install", {"vision": "yes\nrm"}, "control characters")
def test_amd_vision_follows_setups_rules(self):
"""#990: --vision cpu is allowed with the AMD backend on Linux (setup.hip_vision); a GPU encoder is not."""
with mock.patch.object(M, "WIN", False):
self.assertRejected("strata_install", {"backend": "hip", "vision": "yes"}, "no GPU image encoder")
self.assertRejected("strata_install", {"backend": "hip", "vision": "gpu"}, "vision=cpu")
res, err = self.call("strata_install", {"backend": "hip", "vision": "cpu"})
def test_amd_vision_follows_the_picture_rule(self):
"""Owner rule 2026-10-18: pictures are on for AMD too, on Linux and Windows (setup picks the reader: CPU under 24 GB, GPU from 24 GB)."""
for win in (False, True):
with mock.patch.object(M, "WIN", win):
for asked in ("yes", "gpu", "cpu"):
res, err = self.call("strata_install", {"backend": "hip", "vision": asked})
self.assertFalse(err, res)
self.assertEqual(res["plan"]["images"], asked)
self.assertIn("--vision " + asked, res["plan"]["setup_command"])
res, err = self.call("strata_install", {"backend": "hip", "vision": "no"})
self.assertFalse(err, res)
self.assertEqual(res["plan"]["images"], "no")
def test_pictures_default_on(self):
"""Nothing asked: pictures are on where the model can read them (any backend), off for a model that cannot."""
for backend in (None, "cuda", "hip"):
args = {"backend": backend} if backend else {}
res, err = self.call("strata_install", args)
self.assertFalse(err, res)
self.assertEqual(res["plan"]["images"], "cpu")
self.assertIn("--vision cpu", res["plan"]["setup_command"])
res, err = self.call("strata_install", {"backend": "hip", "vision": "no"})
self.assertFalse(err, res)
with mock.patch.object(M, "WIN", True):
self.assertRejected("strata_install", {"backend": "hip", "vision": "cpu"}, "Linux")
res, err = self.call("strata_install", {"backend": "hip", "vision": "no"}) # the Windows AMD engine is no longer refused outright
self.assertFalse(err, res)
self.assertIn("--backend hip", res["plan"]["setup_command"])
self.assertEqual(res["plan"]["images"], "yes")
self.assertIn("--vision yes", res["plan"]["setup_command"])
res, err = self.call("strata_install", {"vision": "no"})
self.assertFalse(err, res)
self.assertEqual(res["plan"]["images"], "no")
def test_data_dir_paths(self):
self.assertRejected("strata_install", {"data_dir": "models"}, "absolute")