mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
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:
1 parent
99cbdd5592
commit
af1ffd913a
6 files changed
+296
-23
No files matched your search
@@ -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__"))
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user