Mac app: Unsloth's sets are offered and installed as the Terminal's setup_mac does (metal_repack to one GGUF, --ple-gguf the table's shard, disk counts the repacked file); the repack tools ship in the installer bundle; tests

This commit is contained in:
Niko1221 committed 2026-11-05 09:05:01 +01:00
1 parent 99cbdd5592
commit af1ffd913a
6 files changed
+296 -23

No files matched your search

+6
View File
@@ -132,6 +132,8 @@ INSTALLER_TOOLS = ("strata_mcp", "iq_pack", "strata_pack", "pack_index", "mtp_fe
#: tests/terminal/test_bundle_tools_paths.py reads the by-path references out of the source and fails when one is missing from the bundle.
INSTALLER_SCRIPTS = ("dsv41/pack_gguf", "dsv41/smoke", "dsv41/tune", "nvfp4_local_pack", "strata_bench", "strata_inspect")
INSTALLER_DATA = "data" # draft vocabularies, expert profiles: 2 MB
#: the Mac install's tools (setup.py's mac_control_setup runs them by path): Unsloth's many-shard sets become ONE GGUF for the Metal engine (stdlib only, 15 KB)
INSTALLER_METAL_TOOLS = ("metal_repack", "stream_experts")
#: packages the bundle must import after the install (the API server's and the CLI's).
PROBE = "import jinja2, yaml, regex, psutil, requests, numpy, PIL, tqdm; print('deps ok')"
@@ -295,6 +297,10 @@ def add_installer(folder: Path, source: Path = ROOT) -> dict:
(folder / "tools" / f"{name}.py").parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source / "tools" / f"{name}.py", folder / "tools" / f"{name}.py")
copied.append(f"tools/{name}.py")
(folder / "metal" / "tools").mkdir(parents=True, exist_ok=True)
for name in INSTALLER_METAL_TOOLS:
shutil.copy2(source / "metal" / "tools" / f"{name}.py", folder / "metal" / "tools" / f"{name}.py")
copied.append(f"metal/tools/{name}.py")
if (folder / INSTALLER_DATA).exists():
shutil.rmtree(folder / INSTALLER_DATA)
shutil.copytree(source / INSTALLER_DATA, folder / INSTALLER_DATA, ignore=shutil.ignore_patterns("__pycache__"))
+105 -8
View File
@@ -9572,6 +9572,88 @@ def prepare_pack_only(a) -> int:
return 0
MAC_PREPARE_LOCK = ".preparing.lock" # beside the repacked file, holds setup's pid from the repack to the end of the pack (metal/setup_mac.py PREPARE_LOCK)
MAC_EXPERTS_MARGIN_GB = 10.0 # what stays free on the disk after the experts' streaming copy (metal/setup_mac.py EXPERTS_MARGIN_GB)
def mac_tools_dir() -> Path:
"""metal/tools: metal_repack.py (many Unsloth shards -> one GGUF for the GPU) and stream_experts.py; both ship in the installer's bundle."""
return Path(ROOT, "metal", "tools")
def mac_ple_shard(files: list) -> Path:
"""The shard that holds the n-gram table (per_layer_token_embd): the repacked file leaves it out and the engine reads it from there (--ple-gguf)."""
sys.path.insert(0, str(mac_tools_dir()))
import metal_repack as R
for f in files:
rd = R.Reader(f)
rd.f.close()
if any(t["name"] == R.PLE for t in rd.tensors):
return f
fail(f"no shard of {files[0].name} holds the n-gram table (per_layer_token_embd)")
def mac_engine_pids() -> list[int]:
"""Pids of this user's running Metal engines (`strata`): a repack beside an engine of 70-90 GB froze the 128 GB M5 Max twice."""
cmd = ["pgrep", "-x", "strata"]
if hasattr(os, "getuid"):
cmd[1:1] = ["-u", str(os.getuid())]
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=10).stdout
except (OSError, subprocess.SubprocessError):
return []
return [int(x) for x in out.split() if x.isdigit() and int(x) != os.getpid()]
def mac_experts_file(native: Path) -> None:
"""A streaming Mac: <model>.experts beside the GPU's GGUF (metal/tools/stream_experts.py), written only when the disk has room (metal/setup_mac.py experts_file)."""
from strata_control import mac_fit as MF
out = native.with_name(native.name + ".experts")
if out.exists() and done(out):
ok(f"the experts' streaming copy: {out.name}")
return
sys.path.insert(0, str(mac_tools_dir()))
import stream_experts as X
need = X.size_bytes(native) / 1e9
free = free_gb(native.parent)
if not os.access(native.parent, os.W_OK):
warn(f"{native.parent} cannot be written: the experts' streaming copy ({need:.0f} GB) is not written; the "
f"engine streams from the model file ({MF.STREAM_COPY_GAIN} slower)")
return
if free < need + MAC_EXPERTS_MARGIN_GB:
warn(f"the experts' streaming copy needs {need:.0f} GB and {MAC_EXPERTS_MARGIN_GB:.0f} GB to spare on the disk "
f"({free:.0f} GB free): not written; the engine streams from the model file ({MF.STREAM_COPY_GAIN} "
f"slower). Free some space and run ./setup.sh --setup again to add it")
return
say(f" Writing the experts in SSD-read order (one time, {need:.0f} GB; each expert is read with one request) ...")
run([sys.executable, str(mac_tools_dir() / "stream_experts.py"), "--gguf", str(native), "--verify"])
mark(out)
ok(f"the experts' streaming copy: {out.name}")
def mac_prepare_lock(native: Path) -> Path:
"""Take the prepare lock beside the repacked file (it holds our pid from the repack to the end of the pack: metal/setup_mac.py's start() and the engine look at it),
and never beside a running engine: an engine of 70-90 GB beside a repack froze the 128 GB M5 Max twice. The caller removes the lock."""
running = mac_engine_pids()
if running:
fail(f"a Strata engine is running (pid {running[0]}): preparing a model beside it can freeze this Mac",
"stop the server (close its window), then run ./setup.sh --setup again")
native.parent.mkdir(parents=True, exist_ok=True)
lock = native.parent / MAC_PREPARE_LOCK
lock.write_text(str(os.getpid()))
return lock
def mac_repack(files: list, model: str, native: Path) -> None:
"""Unsloth's many-shard set as ONE GGUF for the GPU (metal/tools/metal_repack.py --verify: every tensor hashed against its shard; the n-gram table stays
in its shard); the same step as metal/setup_mac.py."""
from strata_control import mac_fit as MF
say(f" Writing the model's experts and weights into one file for the GPU (one time, "
f"{MF.MAC_MODELS[model]['shard1_gb']:.0f} GB) ...")
run([sys.executable, str(mac_tools_dir() / "metal_repack.py"), "--shard1", str(files[0]), "--out", str(native), "--verify"])
mark(native)
def mac_control_setup(a) -> int:
"""The app (and the Terminal's `strata launch`) on a Mac, STRATA_CONTROL_INSTALL=1: the same job as on Windows and Linux. It
downloads and prepares the model files and writes a run config whose engine is "component:strata-engine"; it builds nothing
@@ -9601,8 +9683,8 @@ def mac_control_setup(a) -> int:
fail(problems[0])
step(2, "your choices")
app_sizes = [m for m, d in MF.MAC_MODELS.items() if d.get("family", "qwen") in ("qwen", "gyro")] # Gyro: one file, no repack
if a.family not in (None, "qwen", "gyro"):
app_sizes = [m for f in MF.APP_FAMILIES for m in MF.app_sizes(f)] # Gyro: one file, no repack; Unsloth's sets: the repack below (round 0214)
if a.family not in (None, *MF.APP_FAMILIES):
fail(f"the app on a Mac runs the Qwen3.8-Flash-Next sizes ({', '.join(app_sizes)}); {a.family} is not one of them",
"choose Qwen3.8-Flash-Next")
rec, notes = MF.recommend_model(ram)
@@ -9660,7 +9742,10 @@ def mac_control_setup(a) -> int:
if problem: # files Strata cannot run, or another choice's files
fail(*problem)
mark_whole_unless_pinned(files, model_hf(fam, model) if fam.get("hf") else None, bool(a.gguf_dir)) # a whole file copied in by hand has no finish mark
need = (0 if a.gguf_dir or all(s.exists() and done(s) for s in files) else MODELS[model]["download_gb"]) + 6
repacked = family == "unsloth" and len(files) > 2 # Unsloth's many-shard set: one GGUF for the GPU (mac_repack), the n-gram table left in its shard
native = data / "metal" / f"{tag.lower()}-metal.gguf"
need = MF.install_disk_gb(model, 0 if a.gguf_dir or all(s.exists() and done(s) for s in files) else MODELS[model]["download_gb"]) \
- (MF.repacked_gb(model) if native.exists() and done(native) else 0.0)
if free_gb(models_dir if models_dir.exists() else data) < need:
warn(f"about {need:.0f} GB are needed on the disk: kept as you chose; it may run out")
if not a.gguf_dir:
@@ -9692,9 +9777,20 @@ def mac_control_setup(a) -> int:
step(6, "preparing the model for Strata")
pack = find_in(roots, f"packs/{tag.lower()}") or data / "packs" / tag.lower()
env = dict(os.environ, STRATA_GGUF_PY=str(llama / "gguf-py"))
if not (pack / "native_experts.txt").exists() or not (pack / "tokenizer" / "vocab.json").exists():
run([sys.executable, str(ROOT / "tools" / "iq_pack.py"), "--gguf", str(files[0]), "--out", str(pack),
*fam.get("pack_args", [])], env=env) # Gyro: --compat-bf16
shards = [files[0], files[0]] if family == "gyro" else files # Gyro: one file, the model and its n-gram table
pack_todo = not (pack / "native_experts.txt").exists() or not (pack / "tokenizer" / "vocab.json").exists()
prep_lock = mac_prepare_lock(native) if repacked and (pack_todo or not (native.exists() and done(native))) else None
try:
if repacked:
if not (native.exists() and done(native)):
mac_repack(files, model, native)
shards = [native, mac_ple_shard(files)]
if pack_todo:
run([sys.executable, str(ROOT / "tools" / "iq_pack.py"), "--gguf", str(shards[0]), "--out", str(pack),
*fam.get("pack_args", [])], env=env) # Gyro and Unsloth: --compat-bf16 (the host's copies; the GPU reads the file)
finally:
if prep_lock is not None:
prep_lock.unlink(missing_ok=True)
ok(f"model prepared: {pack}")
use_mtp = MF.stream_mtp(stream, ram)
mtp = (find_in(roots, "mtp/mtp-q2_0.gguf") or data / "mtp" / "mtp-q2_0.gguf").parent
@@ -9719,11 +9815,12 @@ def mac_control_setup(a) -> int:
rt.mkdir(parents=True, exist_ok=True) # the Metal engine reads mtp-q2_0.gguf beside it (no rt pack)
refresh_draft_vocab(rt, a.draft_vocab or "cjk")
ok(f"MTP draft layer: {mtp / 'mtp-q2_0.gguf'}")
if repacked and stream and ram < MF.STREAM_MTP_FROM_GB:
mac_experts_file(shards[0]) # a small Mac: its misses are many; a big Mac's arena holds most experts
step(7, "writing the run settings")
port = a.port or 8080
args = MF.engine_args(pack, [files[0], files[0]] if family == "gyro" else files, rt, ctx, stream, ram,
ROOT / "data" / "expert-profile.bin", model) # Gyro: one file, the model and its n-gram table
args = MF.engine_args(pack, shards, rt, ctx, stream, ram, ROOT / "data" / "expert-profile.bin", model) # Unsloth: --native the repacked file, --ple-gguf the table's shard
cfg = {"exe": engine_exe(Path("engine")), "args": args, "cwd": str(ROOT), "tokenizer": str(pack / "tokenizer"),
"model_name": f"{fam['name']}-{model.lower()}", "log": str(home() / f"strata-{tag.lower()}.log"),
"lib_dirs": [], "port": port, "backend": "metal", "env": {"STRATA_METAL_STREAM": "1" if stream else "0"}}
+23
View File
@@ -183,6 +183,29 @@ def gpu_budget_gb(ram_gb: float) -> float:
return ws
# The families the Mac app (setup.py's mac_control_setup and the app's model list) installs: the GSQ-RCO Qwen files, Unsloth's sets (several shards:
# metal/tools/metal_repack.py writes one GGUF for the GPU) and Gyro. A set with "missing" is left out (METAL_Q5_0_DOWN above).
APP_FAMILIES = ("qwen", "unsloth", "gyro")
# What the repack and the pack work in beside the shards (the Mac install path counts it; the repacked file is MAC_MODELS' shard1_gb).
REPACK_WORK_GB = 6.0
def app_sizes(family: str) -> list[str]:
"""The sizes the Mac app offers for a family: every MAC_MODELS entry of it that the Metal engine can run."""
return [m for m, d in MAC_MODELS.items() if d.get("family", "qwen") == family and not d.get("missing")]
def repacked_gb(model: str) -> float:
"""The disk the repacked GGUF takes (GB): shard1_gb for a many-shard Unsloth set, 0 for a file the engine maps as it is."""
d = MAC_MODELS.get(model, {})
return float(d["shard1_gb"]) if d.get("family") == "unsloth" and int(d.get("shards", 2)) > 2 else 0.0
def install_disk_gb(model: str, download_left_gb: float) -> float:
"""The free disk an install needs: what is still to download, the repacked file, and room to work."""
return float(download_left_gb) + repacked_gb(model) + REPACK_WORK_GB
def model_need_gb(model: str) -> float:
return MAC_MODELS[model]["shard1_gb"] + OVERHEAD_GB
+6 -6
View File
@@ -1105,11 +1105,11 @@ class Strata:
return hw
def _mac_sizes(self, models: dict, family: str = "qwen") -> list:
"""The sizes the app offers on a Mac: the GSQ-RCO files of the Qwen family (two shards, no repack) and agentionai's Gyro files (one
file, no repack). Unsloth's many-shard files need metal/tools/metal_repack.py, which the Terminal's setup uses; the app does not
offer them yet."""
"""The sizes the app offers on a Mac: the GSQ-RCO files of the Qwen family (two shards, no repack), agentionai's Gyro files (one
file, no repack) and, since round 0214, Unsloth's sets (many shards: setup.py's Mac install repacks them with
metal/tools/metal_repack.py, as the Terminal's setup does); a set the Metal engine cannot run yet is left out (mac_fit.MAC_MODELS "missing")."""
mf = self.mac()
return [m for m, d in mf.MAC_MODELS.items() if d.get("family", "qwen") == family and m in models] if mf else []
return [m for m in mf.app_sizes(family) if m in models] if mf else []
def _mac_verdict(self, model: str, ram: float) -> str:
mf = self.mac()
@@ -1153,7 +1153,7 @@ class Strata:
ram = float(hw.get("ram_gb") or 0)
have = {c.stem[len("strata-"):] for c in self.configs()}
out = []
for family in ("qwen", "gyro"): # Gyro after the regular sizes, as on a PC: never the default
for family in (self.mac().APP_FAMILIES if self.mac() else ("qwen", "gyro")): # Qwen, Unsloth's sets, then Gyro: never the default
fd = families.get(family)
if fd is None:
continue
@@ -2423,7 +2423,7 @@ class Tools:
tag = (families[family].get("tag", "") + model)
have_dir = target / "models" / tag
partly = (have_dir.is_dir() and any(have_dir.glob("*.gguf*"))) or bool(gguf_dir) # a local folder: nothing to download
need = (8 if partly else md["download_gb"] + 8) + (1 if vision in ("yes", "gpu", "cpu") else 0)
need = (8 if partly else md["download_gb"] + 8) + (1 if vision in ("yes", "gpu", "cpu") else 0) + (s.mac().repacked_gb(model) if mac and s.mac() else 0) # a Mac repacks Unsloth's shards into one file for the GPU (mac_fit.repacked_gb)
free = s.disk_free(target)
short = None
if free is not None and free < need:
+123 -5
View File
@@ -361,7 +361,7 @@ class ControlMode(unittest.TestCase):
f.with_name(f.name + ".done").write_text("whole")
self.files.append(f)
def run_setup(self, hw=M4, argv=("--yes", "--no-start", "--family", "qwen", "--model", "Q2_0", "--context", "8192"), clt_rc=0):
def run_setup(self, hw=M4, argv=("--yes", "--no-start", "--family", "qwen", "--model", "Q2_0", "--context", "8192"), clt_rc=0, tool=None, free=500.0):
from strata_control import engine_component, mac_fit
ran = []
@@ -379,7 +379,7 @@ class ControlMode(unittest.TestCase):
mock.patch.object(setup, "load_settings", lambda: {}), mock.patch.object(setup, "save_settings", lambda s: None),
mock.patch.object(setup, "get_llama_cpp", lambda: Path(self.tmp.name) / "llama"),
mock.patch.object(setup, "whole_shard", lambda s: True), mock.patch.object(setup, "check_shards", lambda s: None),
mock.patch.object(setup, "run", fake_tool), mock.patch.object(setup, "free_gb", lambda p: 500.0),
mock.patch.object(setup, "run", tool or fake_tool), mock.patch.object(setup, "free_gb", lambda p: free),
mock.patch.object(setup, "say", lambda *a, **k: None), mock.patch.object(setup, "ok", lambda *a, **k: None),
mock.patch.object(setup, "step", lambda *a, **k: None),
mock.patch.object(setup, "warn", lambda *a, **k: self.warnings.append(" ".join(str(x) for x in a))),
@@ -427,11 +427,11 @@ class ControlMode(unittest.TestCase):
self.assertEqual(cfg["env"], {"STRATA_METAL_STREAM": "0"})
self.assertEqual(cfg["args"][cfg["args"].index("--max-context") + 1], "65536") # the default context by memory
def test_no_command_line_tools_needed_and_the_unsloth_files_are_not_offered(self):
def test_no_command_line_tools_needed_and_ud_iq1_s_is_not_offered(self):
rc, _ = self.run_setup(clt_rc=2)
self.assertEqual(rc, 0)
with self.assertRaises(SystemExit):
self.run_setup(argv=("--yes", "--no-start", "--model", "UD-Q4_K_XL"))
with self.assertRaises(SystemExit): # UD-IQ1_S: never offered
self.run_setup(argv=("--yes", "--no-start", "--model", "UD-IQ1_S"))
def test_gyro_is_offered_on_a_mac_as_one_file_with_a_compat_bf16_pack(self):
fam = setup.FAMILIES["gyro"]
@@ -469,6 +469,124 @@ class ControlMode(unittest.TestCase):
self.assertIn('if sys.platform == "darwin" and CONTROL:', src)
class MacAppUnsloth(unittest.TestCase):
"""Round 0214: the Mac app installs Unsloth's sets as the Terminal's setup_mac does: the shards -> ONE GGUF for the GPU (metal_repack.py --verify, the n-gram table
left out), the pack from that file, --native the repacked file, --ple-gguf the table's shard, --mmap-experts. Header-only fixtures (tests/fixtures/unsloth_v31): no
Mac, no weights; the repack and the pack tools are recorded, not run."""
setUp = ControlMode.setUp
run_setup = ControlMode.run_setup
config = ControlMode.config
NEW = ("UD-IQ1_M", "UD-Q2_0", "UD-IQ2_XS", "UD-Q2_K_XL", "UD-IQ3_XXS", "UD-Q3_K_XL", "UD-IQ4_XS-v31")
def shards(self, model):
sys.path.insert(0, str(ROOT / "tools"))
import unsloth_v31_fixture as FX
doc = FX.load()
hub = "UD-IQ4_XS" if model == "UD-IQ4_XS-v31" else model
for old in self.models.glob("*"):
old.unlink()
out = []
for i, s in enumerate(doc["sets"][hub]["shards"]):
h = s["header"] or {"split": [0, len(doc["sets"][hub]["shards"]), 1224], "tensors": []}
p = FX.materialize({"header": h}, self.models / s["name"], first=(i == 0))
p.with_name(p.name + ".done").write_text("whole")
out.append(p)
return out
def tool(self, ran):
def fake(cmd, **kw):
ran.append([str(x) for x in cmd])
if cmd[1].endswith("metal_repack.py"):
Path(cmd[cmd.index("--out") + 1]).write_bytes(b"x")
else:
pack = Path(cmd[cmd.index("--out") + 1])
(pack / "tokenizer").mkdir(parents=True, exist_ok=True)
(pack / "native_experts.txt").write_text("x")
(pack / "tokenizer" / "vocab.json").write_text("{}")
return fake
def install(self, model, hw=M5, **kw):
ran = []
engines = kw.pop("engines", [])
with mock.patch.object(setup, "mac_engine_pids", lambda: engines):
rc, _ = self.run_setup(hw=hw, argv=("--yes", "--no-start", "--family", "unsloth", "--model", model, "--context", "8192", "--mtp", "off"),
tool=self.tool(ran), **kw)
return rc, ran
def test_the_repack_the_pack_and_the_config(self):
for model in self.NEW:
with self.subTest(model=model):
files = self.shards(model)
shutil_native = self.data / "metal" / f"unsloth-{model.lower()}-metal.gguf"
if shutil_native.exists():
shutil_native.unlink()
shutil_native.with_name(shutil_native.name + ".done").unlink(missing_ok=True)
rc, ran = self.install(model)
self.assertEqual(rc, 0)
self.assertEqual(len(ran), 2, ran)
repack, pack = ran
self.assertTrue(repack[1].endswith("metal_repack.py"))
self.assertEqual(repack[repack.index("--shard1") + 1], str(files[0]))
self.assertEqual(repack[repack.index("--out") + 1], str(shutil_native))
self.assertIn("--verify", repack)
self.assertTrue(pack[1].endswith("iq_pack.py"))
self.assertEqual(pack[pack.index("--gguf") + 1], str(shutil_native)) # the pack is read from the repacked file
self.assertIn("--compat-bf16", pack)
cfg = self.config("unsloth-" + model.lower())
a = cfg["args"]
self.assertEqual(cfg["exe"], "component:strata-engine")
self.assertEqual(a[a.index("--native") + 1], str(shutil_native))
self.assertEqual(a[a.index("--native-dense-gguf") + 1], str(shutil_native))
self.assertEqual(a[a.index("--ple-gguf") + 1], str(files[-1])) # the n-gram table: the LAST shard of these sets
self.assertIn("--mmap-experts", a)
self.assertEqual(cfg["env"]["STRATA_METAL_STREAM"], "1" if M.streams(model, 128.0) else "0")
self.assertEqual(cfg["tokenizer"], str(self.data / "packs" / ("unsloth-" + model.lower()) / "tokenizer"))
self.assertEqual(cfg["model_name"], "qwen3.8-flash-next-unsloth-" + model.lower())
self.assertFalse((shutil_native.parent / ".preparing.lock").exists(), "the prepare lock is gone")
def test_an_already_repacked_set_is_not_repacked_again(self):
model = "UD-IQ1_M"
self.shards(model)
native = self.data / "metal" / "unsloth-ud-iq1_m-metal.gguf"
native.parent.mkdir(parents=True)
native.write_bytes(b"x")
setup.mark(native)
rc, ran = self.install(model)
self.assertEqual(rc, 0)
self.assertEqual([r[1].rsplit("/", 1)[-1] for r in ran], ["iq_pack.py"])
def test_a_running_engine_stops_the_repack_before_anything_is_written(self):
self.shards("UD-IQ1_M")
with self.assertRaises(SystemExit):
self.install("UD-IQ1_M", engines=[4242])
self.assertFalse((self.data / "metal").exists() and any((self.data / "metal").iterdir()))
def test_the_disk_check_counts_the_repacked_file(self):
self.shards("UD-IQ1_M")
self.install("UD-IQ1_M", free=30.0)
said = [w for w in self.warnings if "are needed on the disk" in w]
self.assertEqual(len(said), 1, self.warnings)
self.assertIn("about 45 GB", said[0]) # nothing to download (the files are here), the 39.2 GB repacked file and 6 GB to work in
def test_a_set_the_engine_cannot_run_is_not_offered(self):
from strata_control import mac_fit
self.shards("UD-Q3_K_XL")
with mock.patch.dict(mac_fit.MAC_MODELS, {"UD-Q3_K_XL": {**mac_fit.MAC_MODELS["UD-Q3_K_XL"], "missing": "the Q5_0 expert kernels"}}):
with self.assertRaises(SystemExit):
self.install("UD-Q3_K_XL")
def test_the_bundle_carries_the_repack_tools(self):
sys.path.insert(0, str(ROOT / "packaging"))
import package_terminal as pt
self.assertEqual(pt.INSTALLER_METAL_TOOLS, ("metal_repack", "stream_experts"))
for n in pt.INSTALLER_METAL_TOOLS:
self.assertTrue((ROOT / "metal" / "tools" / f"{n}.py").is_file())
with tempfile.TemporaryDirectory() as t:
out = pt.add_installer(Path(t))
self.assertIn("metal/tools/metal_repack.py", out["files"])
self.assertTrue((Path(t) / "metal" / "tools" / "stream_experts.py").is_file())
class UpdateFlow(unittest.TestCase):
"""update.sh asks the same questions as a setup run but does not download model files: a model chosen there must
be named, not dropped in silence (an M1 Max picked IQ3_S in update.sh and got no model and no message)."""
+33 -4
View File
@@ -769,11 +769,16 @@ class MacCatalog(unittest.TestCase):
def test_the_model_list_is_the_macs_sizes_with_unified_memory_verdicts(self):
s, hw = self.hw(32)
fams = s.model_table(hw)
self.assertEqual([f["family"] for f in fams], ["qwen", "gyro", "deepseek41"],
"no Unsloth, Swift or NVFP4 on a Mac in the app; Gyro after the regular sizes, DeepSeek (0.2.7.1) last")
self.assertEqual([f["family"] for f in fams], ["qwen", "unsloth", "gyro", "deepseek41"],
"Unsloth's sets since round 0214 (the app repacks them), no Swift or NVFP4 on a Mac; Gyro after the regular sizes, DeepSeek (0.2.7.1) last")
by = {x["model"]: x for x in fams[0]["sizes"]}
self.assertEqual(list(by), ["Q2_0", "IQ3_XXS", "IQ3_S"])
gy = {x["model"]: x for x in fams[1]["sizes"]}
un = {x["model"]: x for x in fams[1]["sizes"]}
self.assertEqual(list(un), ["UD-IQ1_M", "UD-Q2_0", "UD-IQ2_XS", "UD-Q2_K_XL", "UD-IQ3_XXS", "UD-Q3_K_XL", "UD-IQ4_XS-v31", "UD-Q4_K_XL", "UD-Q5_K_XL", "UD-Q6_K_XL"])
self.assertEqual((un["UD-IQ1_M"]["download_gb"], un["UD-IQ1_M"]["experts_gb"]), (68.0, 34.8))
self.assertTrue(all(x["on_this_pc"] and x["images"] and x["id"] == "unsloth-" + x["model"].lower() for x in un.values()))
self.assertTrue(un["UD-Q5_K_XL"]["on_this_pc"].startswith("slow here"), "32 GB: 104 GB to map does not fit, said not refused")
gy = {x["model"]: x for x in fams[2]["sizes"]}
self.assertEqual(list(gy), ["Gyro-S", "Gyro-M"])
self.assertEqual(gy["Gyro-S"]["download_gb"], 58.5)
self.assertTrue(all(v["on_this_pc"] and "SSD" in v["on_this_pc"] for v in gy.values()), "32 GB: both stream (said, not refused)")
@@ -783,7 +788,11 @@ class MacCatalog(unittest.TestCase):
self.assertTrue(by["IQ3_S"]["on_this_pc"].startswith("slow here"))
self.assertTrue(all(x["images"] for x in by.values()), "the Mac engine carries the Metal picture reader: pictures are on")
s, hw = self.hw(128)
self.assertEqual({x["on_this_pc"] for f in s.model_table(hw) if f["family"] != "deepseek41" for x in f["sizes"]}, {"fits"})
fits = {x["model"]: x["on_this_pc"] for f in s.model_table(hw) if f["family"] != "deepseek41" for x in f["sizes"]}
from strata_control import mac_fit
for m, v in fits.items(): # the 128 GB Mac keeps ~96 GB resident: UD-Q5_K_XL and UD-Q6_K_XL (104 / 115 GB to map) stream, said not refused
self.assertEqual(v == "fits", not mac_fit.streams(m, 128.0), m)
self.assertEqual({m for m, v in fits.items() if v != "fits"}, {"UD-Q5_K_XL", "UD-Q6_K_XL"})
s, hw = self.hw(8)
self.assertEqual({x["on_this_pc"] for f in s.model_table(hw) if f["family"] != "deepseek41" for x in f["sizes"]}, {"does not fit"})
self.assertTrue(all(x["on_this_pc"].startswith("does not fit") for f in s.model_table(hw) if f["family"] == "deepseek41" for x in f["sizes"]))
@@ -864,6 +873,26 @@ class MacCatalog(unittest.TestCase):
s2.hardware = lambda fresh=False: hw2
M.Tools(s2).install_plan(None, None, None, None, None, None, None, None, None, None, None)
def test_the_install_plan_counts_the_repacked_file_of_an_unsloth_set(self):
"""Round 0214: the Mac app repacks Unsloth's shards into one GGUF: the disk it needs is the download, that file (mac_fit shard1_gb) and room to work."""
s, hw = self.hw(128)
s.hardware = lambda fresh=False: hw
s.disk_free = lambda p: 1000.0
with tempfile.TemporaryDirectory() as t:
target = Path(t) / "Strata-data"
target.mkdir()
plan, args = M.Tools(s).install_plan("unsloth", "UD-IQ1_M", None, "no", "int8", 0, None, None, "auto", None, str(target))
plain, _ = M.Tools(s).install_plan("qwen", "Q2_0", None, "no", "int8", 0, None, None, "auto", None, str(target))
self.assertEqual(plan["disk_needed_gb"], round(68.0 + 8 + 39.2))
self.assertEqual(plain["disk_needed_gb"], round(plain["download_gb"] + 8)) # no repack for a GSQ-RCO file
self.assertEqual(args[:6], ["--yes", "--no-start", "--family", "unsloth", "--model", "UD-IQ1_M"])
s.disk_free = lambda p: 100.0
with tempfile.TemporaryDirectory() as t:
target = Path(t) / "Strata-data"
target.mkdir()
short, _ = M.Tools(s).install_plan("unsloth", "UD-IQ1_M", None, "no", "int8", 0, None, None, "auto", None, str(target))
self.assertIn("not enough free disk space", short["disk_short"])
def test_the_install_plan_for_deepseek_on_a_mac_is_text_only_at_8k(self):
"""(with an engine that has no Metal picture tower)"""
s, hw = self.hw(128)