diff --git a/strata_control/dsv41_fit.py b/strata_control/dsv41_fit.py index a655d92e5..4b7c42977 100644 --- a/strata_control/dsv41_fit.py +++ b/strata_control/dsv41_fit.py @@ -515,6 +515,7 @@ MAC_PACKS: dict[str, dict[str, Any]] = { "IQ2_XXS": {"name": "2.00 bpw (IQ2_XXS), the default for most users", "routed_gb": 132.07, "backbone_gb": 138.24, "bpw": 2.00}, "IQ2_XS": {"name": "2.25 bpw (IQ2_XS), the quality one", "routed_gb": 149.06, "backbone_gb": 155.23, "bpw": 2.25}, "UD-IQ1_S": {"name": "Unsloth UD-IQ1_S (1-bit experts)", "routed_gb": 115.23, "backbone_gb": 122.38, "bpw": 1.75}, + "UD-IQ1_M": {"name": "Unsloth UD-IQ1_M (1-bit experts)", "routed_gb": 132.21, "backbone_gb": 139.81, "bpw": 2.00}, } MAC_ENGRAM_GB = 110.60 # the same Engram table for all three, read row by row off the SSD MAC_ENGINE_FOOTPRINT_GB = 12.0 # the process' anonymous memory after the load (Metal buffers, host buffers, the KV cache) @@ -533,6 +534,7 @@ MAC_MEASURED_PACK = { "IQ2_XS": MAC_MEASURED, "UD-IQ1_S": ("M5 Max, 128 GB, Unsloth UD-IQ1_S: 15-18 tokens/s for a short answer (16-18 after a 3,000-token prompt), a 3,000-token prompt at " "about 230-310 tokens/s, 14,000 at about 520; the engine kept about 68 GB of the experts resident and the process used about 79 GB"), + "UD-IQ1_M": "Not run on a Mac yet; UD-IQ1_S was. It keeps fewer experts in memory, so expect it to be slower.", } @@ -612,11 +614,11 @@ def mac_plan(ram_gb: float, quant: str = "IQ2_XS", disk_free_gb: float | None = # the 1.75 bpw set shares the 2.00 set's IQ1_M kernel but was never run there, so it is not offered on a Mac. -MAC_SET_PACK = {"IST-DAS-2.00bpw": "IQ2_XXS", "IST-DAS-2.25bpw": "IQ2_XS", "UD-IQ1_S": "UD-IQ1_S"} -# Unsloth's UD-IQ1_S runs on the Metal engine since the IQ1_S kernels (f-039-mac-dsu). UD-IQ1_M has the same kernels but was never run on a Mac, so it is kept off -# the Mac everywhere: the model lists (MAC_SET_PACK above does that on its own), setup, the launch and the "I already have the model" check, with this one line. -# Remove a name here when its set has run on a Mac. -MAC_NOT_YET = ("UD-IQ1_M",) +MAC_SET_PACK = {"IST-DAS-2.00bpw": "IQ2_XXS", "IST-DAS-2.25bpw": "IQ2_XS", "UD-IQ1_S": "UD-IQ1_S", "UD-IQ1_M": "UD-IQ1_M"} +# Unsloth's UD-IQ1_S was run on an M5 Max (f-039-mac-dsu). UD-IQ1_M has the same kernels and was not run there; the owner chose to offer it anyway ("they +# will tell if it works"), with a plain "not run yet" line in its plan. The gate stays as a mechanism, empty: put a name here to keep a set off a Mac +# (the model lists, setup, the launch and the "I already have the model" check all read it through not_here()). +MAC_NOT_YET = () MAC_NOT_YET_LINE = "Unsloth's UD-IQ1_M set doesn't run on a Mac yet; the UD-IQ1_S, 2.00 and 2.25 sets do." # The same two sets were kept off a DGX Spark (linux-aarch64, GB10) until a Spark run passed: both passed there on 2026-10-30 (real `strata launch --model-path`, # 10 of 10 answers, tool call correct), so the tuple is empty. Put a name back here to hide a set from the Spark again. @@ -672,8 +674,9 @@ def _plan_mac(key: str, model: str, card: dict, ram_gb: float | None, disk_free_ lines.append(f"DeepSeek-V4.1-Flash, {v['title']}: a {download:.0f} GB download ({v['set_bytes'] / GB:.0f} GB of model, " f"{ISTA_ENGRAM_BYTES / GB:.0f} GB n-gram table, {ISTA_DSPARK_BYTES / GB:.0f} GB drafter). Quality: {v['quality']}.") lines.extend(l for l in mp["lines"][1:] if not l.startswith("Measured on ")) - lines.append("Measured on " + MAC_MEASURED_PACK[MAC_SET_PACK[key]] + ".") - out["measured"] = MAC_MEASURED_PACK[MAC_SET_PACK[key]] + measured = MAC_MEASURED_PACK[MAC_SET_PACK[key]] + lines.append(measured if measured.startswith("Not run") else "Measured on " + measured + ".") + out["measured"] = measured warns.extend(mp["warnings"]) if ram and ram < MAC_SMALL_RAM_GB and mp["verdict"] != "small": warns.append(f"This Mac has {ram:.0f} GB of memory and the set was measured on one with 128 GB: with less, more experts come from " diff --git a/tests/terminal/test_dsv41_mac_unsloth.py b/tests/terminal/test_dsv41_mac_unsloth.py index 482deed44..c8f9ff3b0 100644 --- a/tests/terminal/test_dsv41_mac_unsloth.py +++ b/tests/terminal/test_dsv41_mac_unsloth.py @@ -20,16 +20,29 @@ from tests.terminal.launch_support import HW_12, Home, catalog LINE = "Unsloth's UD-IQ1_M set doesn't run on a Mac yet; the UD-IQ1_S, 2.00 and 2.25 sets do." MAC = {**HW_12, "mac": True, "ram_gb": 128.0, "disk_free_gb": 900.0, "gpus": [], "engine_vendor": "apple"} PC = {**HW_12, "ram_gb": 64.0, "disk_free_gb": 900.0, "engine_vendor": "nvidia"} -UD = ("UD-IQ1_M",) # the sets kept off a Mac +UD = ("UD-IQ1_M",) # the set the gate holds back in these tests (MAC_NOT_YET is empty in the product: the owner offers UD-IQ1_M on a Mac, not run there) RUNS = "UD-IQ1_S" # the one that was run there BOTH = ("UD-IQ1_S", "UD-IQ1_M") # both ran on a DGX Spark (10-30): AARCH64_NOT_YET is empty; SparkGate patches it back to test the mechanism +def hidden(fn): + """Run a test with the Mac gate as it was while UD-IQ1_M was kept off a Mac (MAC_NOT_YET = UD, and no Mac fit words for it).""" + import functools + + @functools.wraps(fn) + def run(*a, **k): + pack = {m: v for m, v in dsv41_fit.MAC_SET_PACK.items() if m not in UD} + with mock.patch.object(dsv41_fit, "MAC_NOT_YET", UD), mock.patch.object(dsv41_fit, "MAC_SET_PACK", pack): + return fn(*a, **k) + return run + + def ud_files(model): return {**dsv41_sets.tables()["sets"][model]["shards"], **dsv41_sets.tables()["sets"][model]["engram_alt"]} class Line(unittest.TestCase): + @hidden def test_the_line_and_the_names(self): self.assertEqual(dsv41_fit.MAC_NOT_YET, UD) self.assertEqual(dsv41_fit.MAC_NOT_YET_LINE, LINE) @@ -48,6 +61,7 @@ class Lists(unittest.TestCase): def models(self): return [r.model for r in catalog(self.hw).rows() if r.family == "deepseek41"] + @hidden def test_a_mac_is_not_offered_the_unsloth_sets(self): got = self.models() for m in UD: @@ -67,6 +81,7 @@ class Lists(unittest.TestCase): for m in UD + (RUNS,): self.assertIn(m, got) + @hidden def test_model_flag_on_a_mac_says_what_works(self): with self.assertRaises(ValueError) as cm: catalog(self.hw).resolve("ud-iq1_m", None) @@ -96,6 +111,7 @@ class OwnFiles(unittest.TestCase): (folder / name).write_bytes(b"GGUF") return folder + @hidden def test_model_path_of_a_ud_folder_on_a_mac_stops_with_the_line(self): for model in UD: folder = self.folder(model) @@ -120,12 +136,62 @@ class OwnFiles(unittest.TestCase): self.assertEqual((r.s.family, r.s.model), ("deepseek41", "UD-IQ1_S")) +class OfferedOnAMac(unittest.TestCase): + """The product state: the Mac gate is empty, both Unsloth sets are offered; UD-IQ1_M says plainly that it was not run on a Mac.""" + PLAIN = "Not run on a Mac yet; UD-IQ1_S was. It keeps fewer experts in memory, so expect it to be slower." + + def setUp(self): + self.home = Home().__enter__() + self.addCleanup(self.home.__exit__, None, None, None) + self.hw = dict(MAC) + p = mock.patch.object(Catalog, "hardware", lambda s, fresh=False: dict(self.hw)) + p.start() + self.addCleanup(p.stop) + + def test_the_gate_is_empty_and_both_sets_have_a_pack(self): + self.assertEqual(dsv41_fit.MAC_NOT_YET, ()) + for m in ("UD-IQ1_S", "UD-IQ1_M"): + self.assertIn(m, dsv41_fit.MAC_SET_PACK) + self.assertIsNone(dsv41_fit.not_here(m)) + + def test_the_list_and_the_flag(self): + got = [r.model for r in catalog(self.hw).rows() if r.family == "deepseek41"] + for m in ("UD-IQ1_S", "UD-IQ1_M"): + self.assertIn(m, got) + self.assertEqual(catalog(self.hw).resolve("ud-iq1_m"), ("deepseek41", "UD-IQ1_M")) + + def test_the_plan_of_ud_iq1_m_says_it_was_not_run_and_invents_no_numbers(self): + plan = dsv41_fit.plan([{"vendor": "apple", "unified_gb": 128.0, "name": "Apple M5 Max"}], 128.0, 900.0, model="UD-IQ1_M") + self.assertTrue(plan.get("mac")) + self.assertIn(self.PLAIN, plan["words"]) + self.assertEqual(plan["measured"], self.PLAIN) + self.assertNotIn("tokens/s", plan["words"]) + self.assertAlmostEqual(plan["download_gb"], 258.0, delta=1.0) # with the drafter and the Engram + s = dsv41_fit.plan([{"vendor": "apple", "unified_gb": 128.0, "name": "Apple M5 Max"}], 128.0, 900.0, model="UD-IQ1_S") + self.assertIn("tokens/s", s["words"]) + + def test_own_files_and_the_preview_on_a_mac(self): + folder = Path(os.environ["STRATA_INSTALL_DIR"]) / "UD-IQ1_M" + folder.mkdir(parents=True, exist_ok=True) + for name in dsv41_sets.tables()["sets"]["UD-IQ1_M"]["shards"]: + (folder / name).write_bytes(b"GGUF") + with mock.patch.object(sys, "platform", "darwin"): + r = Runner(S.LaunchSettings(model_path=str(folder)), catalog=catalog(self.hw)) + r.check_own_files() + self.assertEqual((r.s.family, r.s.model), ("deepseek41", "UD-IQ1_M")) + with mock.patch.object(models, "_dsv41_files", return_value=ud_files("UD-IQ1_M")), mock.patch.object(sys, "platform", "darwin"), __import__("tempfile").TemporaryDirectory() as tmp: + plan = models.plan_import(tmp) + self.assertEqual(plan["file_set_status"], "plausible", plan["issues"]) + self.assertEqual(plan["candidate_selection"]["model"], "UD-IQ1_M") + + class AppCheck(unittest.TestCase): def preview(self, model, platform): with mock.patch.object(models, "_dsv41_files", return_value=ud_files(model)), mock.patch.object(sys, "platform", platform), \ __import__("tempfile").TemporaryDirectory() as tmp: return models.plan_import(tmp) + @hidden def test_on_a_mac_the_preview_says_so_and_selects_nothing(self): for model in UD: plan = self.preview(model, "darwin") @@ -182,6 +248,7 @@ class Setup(unittest.TestCase): return type(exc).__name__, out.getvalue() return None, out.getvalue() + @hidden def test_a_mac_is_stopped_before_anything_else(self): for model in UD: exc, text = self.run_setup(model, "darwin") @@ -228,7 +295,7 @@ class SparkGate(unittest.TestCase): self.assertIsNone(dsv41_fit.not_here("UD-IQ1_S")) with mock.patch.object(sys, "platform", "darwin"): self.assertIsNone(dsv41_fit.not_here("UD-IQ1_S")) - self.assertEqual(dsv41_fit.not_here("UD-IQ1_M"), LINE) + self.assertIsNone(dsv41_fit.not_here("UD-IQ1_M")) # offered on a Mac (not run there) def test_not_in_the_list_on_a_spark_but_on_a_pc(self): with spark(): diff --git a/tests/terminal/test_launch.py b/tests/terminal/test_launch.py index 530331492..ed7a4427c 100644 --- a/tests/terminal/test_launch.py +++ b/tests/terminal/test_launch.py @@ -144,7 +144,7 @@ class CatalogRules(unittest.TestCase): with Home(): cat = catalog(hw) rows = {r.model: r for r in cat.rows()} - self.assertEqual(set(rows), {"Q2_0", "IQ3_XXS", "IQ3_S", "Gyro-S", "Gyro-M", "IST-DAS-2.00bpw", "IST-DAS-2.25bpw", "UD-IQ1_S"}) # DeepSeek (0.2.7.1): the two IST-DAS sets run on a Mac, UD-IQ1_S since f-039 + self.assertEqual(set(rows), {"Q2_0", "IQ3_XXS", "IQ3_S", "Gyro-S", "Gyro-M", "IST-DAS-2.00bpw", "IST-DAS-2.25bpw", "UD-IQ1_S", "UD-IQ1_M"}) # DeepSeek (0.2.7.1): the two IST-DAS sets run on a Mac, UD-IQ1_S since f-039 self.assertEqual([r.model for r in rows.values() if r.recommended], ["Q2_0"]) for name in ("IST-DAS-2.00bpw", "IST-DAS-2.25bpw"): # a 32 GB Mac: said plainly (mostly read from the SSD), never refused self.assertEqual(rows[name].family, "deepseek41")