mirror of
https://github.com/Heretek-AI/Strata.git
synced 2026-11-09 17:16:49 -10:00
234 lines
12 KiB
Python
234 lines
12 KiB
Python
"""Tests for setup.py's model menu (0.1.44): the family order, NVFP4 hidden on AMD and Intel, the NVFP4 line's card note
|
|
(RTX 50 native FP4, older RTX slower than UD-IQ4_XS), the Unsloth sizes in order with images on, and the prebuilt
|
|
install removing a stale strata_kq library.
|
|
|
|
python -m unittest tools.test_setup_menu
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
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
|
|
|
|
|
|
class FamilyOrder(unittest.TestCase):
|
|
def test_the_menu_order_on_nvidia(self):
|
|
fams = setup.menu_families(False) + list(setup.OWN_PATH_FAMILIES)
|
|
self.assertEqual(fams, ["qwen", "swift", "coder", "unsloth", "gyro", "nvfp4", "qwen36"])
|
|
|
|
def test_the_default_stays_the_first_family(self):
|
|
self.assertEqual(setup.menu_families(False)[0], "qwen")
|
|
self.assertIn("GSQ-RCO", setup.FAMILIES["qwen"]["by"])
|
|
|
|
def test_nvfp4_is_not_listed_on_amd_and_intel(self):
|
|
fams = setup.menu_families(True)
|
|
self.assertNotIn("nvfp4", fams)
|
|
self.assertEqual(fams, ["qwen", "swift", "coder", "unsloth", "gyro"])
|
|
self.assertIn("nvfp4", setup.FAMILIES) # --family nvfp4 still exists, and says why not
|
|
|
|
def test_the_unsloth_line_names_the_alternative_on_amd_and_intel(self):
|
|
self.assertIn("UD-IQ4_XS", setup.family_menu_note("unsloth", True, None))
|
|
self.assertEqual(setup.family_menu_note("unsloth", False, {"arch": 86}), "")
|
|
self.assertEqual(setup.family_menu_note("coder", True, None), "")
|
|
|
|
|
|
class Nvfp4Note(unittest.TestCase):
|
|
def test_rtx_50_has_native_fp4(self):
|
|
line = setup.family_menu_note("nvfp4", False, {"arch": 120, "name": "RTX 5070"})
|
|
self.assertIn("native FP4 on this card", line)
|
|
|
|
def test_older_rtx_runs_but_is_slower_than_ud_iq4_xs(self):
|
|
for arch in (75, 86, 89):
|
|
line = setup.family_menu_note("nvfp4", False, {"arch": arch})
|
|
self.assertIn("runs, but UD-IQ4_XS is faster on this card", line, arch)
|
|
self.assertNotIn("native FP4", line)
|
|
|
|
def test_an_avx2_cpu_is_told_its_rows_but_not_refused(self): # #2347
|
|
line = setup.family_menu_note("nvfp4", False, {"arch": 120}, avx2=True, avx512=False)
|
|
self.assertIn("no AVX-512", line)
|
|
self.assertIn("AVX2", line)
|
|
self.assertIn("native FP4 on this card", line) # the card part is unchanged
|
|
self.assertTrue(setup.nvfp4_cpu_note(True, False))
|
|
|
|
def test_an_avx512_cpu_and_a_cpu_without_avx2_get_no_cpu_note(self):
|
|
self.assertEqual(setup.nvfp4_cpu_note(True, True), "")
|
|
self.assertEqual(setup.nvfp4_cpu_note(False, False), "") # the older-CPU build says its own piece
|
|
line512 = setup.family_menu_note("nvfp4", False, {"arch": 120})
|
|
self.assertNotIn("AVX", line512)
|
|
self.assertEqual(line512, setup.family_menu_note("nvfp4", False, {"arch": 120}, avx2=True, avx512=True))
|
|
|
|
def test_the_line_gives_the_download_and_the_ram_need(self):
|
|
line = setup.family_menu_note("nvfp4", False, {"arch": 120})
|
|
d = setup.MODELS["NVFP4"]
|
|
self.assertIn(f"{d['download_gb']:.0f} GB download", line)
|
|
self.assertIn(f"~{d['ram_gb']} GB of RAM", line)
|
|
|
|
|
|
class UnslothSizes(unittest.TestCase):
|
|
def test_the_sizes_in_menu_order(self):
|
|
names = [m for m in setup.MODELS if "unsloth" in setup.MODELS[m].get("families", ())]
|
|
self.assertEqual(names, ["UD-IQ4_XS", "UD-Q4_K_XL", "UD-Q5_K_XL", "UD-Q6_K_XL", "Q8_0"])
|
|
|
|
def test_nothing_is_experimental_and_images_are_on(self):
|
|
for m in ("UD-IQ4_XS", "UD-Q4_K_XL", "UD-Q5_K_XL", "UD-Q6_K_XL", "Q8_0"):
|
|
d = setup.MODELS[m]
|
|
self.assertFalse(d.get("experimental"), m)
|
|
self.assertFalse(d.get("nvidia_only"), m)
|
|
self.assertIs(d.get("vision"), True, m)
|
|
self.assertIn("download_gb", d)
|
|
self.assertIn("ram_gb", d)
|
|
self.assertNotIn("experimental", setup.MODELS["UD-Q4_K_XL"]["about"].lower())
|
|
self.assertTrue(setup.FAMILIES["unsloth"]["vision"])
|
|
|
|
def test_the_family_line_lists_every_size(self):
|
|
about = setup.FAMILIES["unsloth"]["about"]
|
|
for m in ("UD-IQ4_XS", "UD-Q4_K_XL", "UD-Q5_K_XL", "UD-Q6_K_XL", "Q8_0"):
|
|
self.assertIn(m, about)
|
|
|
|
|
|
class RecommendedSizes(unittest.TestCase):
|
|
"""0.1.44 (owner): IQ3_S, UD-Q4_K_XL and UD-IQ4_XS are the recommended sizes; recommend, never force."""
|
|
|
|
def names(self, family):
|
|
return [m for m in setup.MODELS if family in setup.MODELS[m].get("families", ("qwen", "swift"))]
|
|
|
|
def test_qwen_and_swift_default_to_iq3_s_when_it_fits(self):
|
|
for fam in ("qwen", "swift"):
|
|
n = self.names(fam)
|
|
self.assertIn("IQ3_S", n)
|
|
self.assertEqual(setup.recommended_size(fam, n, 67.7), "IQ3_S")
|
|
self.assertEqual(setup.recommended_size(fam, n, 191.0), "IQ3_S") # not the largest: IQ3_S stays the pick
|
|
self.assertEqual([m for m in n if setup.size_tag(fam, m)], ["IQ3_S"]) # the others are choices, unmarked
|
|
|
|
def test_without_the_ram_the_largest_that_fits_and_iq3_s_stays_marked(self):
|
|
n = self.names("qwen")
|
|
self.assertEqual(setup.recommended_size("qwen", n, 61.0), "IQ3_XXS") # IQ3_S needs 62 GB
|
|
self.assertEqual(setup.recommended_size("qwen", n, 52.0), "IQ3_XXS") # 48 GB tier (RAM + VRAM - 5 GB)
|
|
self.assertEqual(setup.recommended_size("qwen", n, 40.0, 11.0), "IQ3_XXS") # 40 + 11 - 5 = 46 GB together
|
|
self.assertEqual(setup.recommended_size("qwen", n, 32.0, 12.0), "IQ2_XS") # 44 - 5 = 39 GB: the 40 GB tier
|
|
self.assertEqual(setup.recommended_size("swift", self.names("swift"), 32.0, 12.0), "IQ2_XS")
|
|
self.assertEqual(setup.recommended_size("qwen", n, 32.0, 35.0), "IQ3_S") # 67 - 5 = 62 GB together
|
|
self.assertEqual(setup.size_tag("qwen", "IQ3_S"), " (recommended)")
|
|
|
|
def test_3425_the_tiers_leave_5_gb_for_windows(self):
|
|
# the reporter: RTX 5080 (16 GB) + 32 GB of RAM summed to 48 GB and got IQ3_XXS, which read ~11 GB from the SSD
|
|
n = self.names("qwen")
|
|
self.assertEqual(setup.TIER_SYSTEM_RESERVE_GB, 5.0)
|
|
self.assertEqual(setup.recommended_size("qwen", n, 31.9, 15.9), "IQ2_XS")
|
|
self.assertEqual(setup.tier_pick(31.9, 15.9), ("swift", "IQ2_XS"))
|
|
# boundaries: floors 38 / 46 / 62 plus the 5 GB
|
|
self.assertEqual(setup.tier_pick(43.0), ("swift", "IQ2_XS"))
|
|
self.assertIsNone(setup.tier_pick(42.9))
|
|
self.assertEqual(setup.tier_pick(51.0), ("qwen", "IQ3_XXS"))
|
|
self.assertEqual(setup.tier_pick(50.9), ("swift", "IQ2_XS"))
|
|
self.assertEqual(setup.tier_pick(67.0), ("qwen", "IQ3_S"))
|
|
self.assertEqual(setup.tier_pick(66.9), ("qwen", "IQ3_XXS"))
|
|
self.assertEqual(setup.tier_pick(40.0, 3.0), ("swift", "IQ2_XS")) # VRAM counts the same as RAM
|
|
|
|
def test_coder_has_nothing_marked(self):
|
|
n = self.names("coder")
|
|
self.assertEqual([setup.size_tag("coder", m) for m in n], [""] * len(n))
|
|
self.assertEqual(setup.recommended_size("coder", n, 64.0), n[0])
|
|
|
|
|
|
class UnslothDefaultEnv(unittest.TestCase):
|
|
"""0.1.44 (owner): the env setup writes for Unsloth's files (review-quants.md section 5)."""
|
|
|
|
def test_q5_and_q6_turn_the_k_quant_prompt_kernels_on_q4_does_not(self):
|
|
f = setup.unsloth_default_env
|
|
self.assertEqual(f("unsloth", "UD-Q5_K_XL", False, False)["STRATA_MMQ_KQ"], "1")
|
|
self.assertEqual(f("unsloth", "UD-Q6_K_XL", True, True)["STRATA_MMQ_KQ"], "1")
|
|
for m in ("UD-Q4_K_XL", "UD-IQ4_XS", "Q8_0"):
|
|
self.assertNotIn("STRATA_MMQ_KQ", f("unsloth", m, False, False), m)
|
|
|
|
def test_hc_q8_on_cuda_for_the_ud_files_only(self):
|
|
f = setup.unsloth_default_env
|
|
for m in ("UD-IQ4_XS", "UD-Q4_K_XL", "UD-Q5_K_XL", "UD-Q6_K_XL"):
|
|
self.assertEqual(f("unsloth", m, False, True)["STRATA_HC_Q8"], "1", m)
|
|
self.assertNotIn("STRATA_HC_Q8", f("unsloth", m, True, True), m) # not on AMD
|
|
self.assertNotIn("STRATA_HC_Q8", f("unsloth", "Q8_0", False, True))
|
|
|
|
def test_tail_skip_off_only_when_every_expert_is_in_ram(self):
|
|
f = setup.unsloth_default_env
|
|
self.assertEqual(f("unsloth", "UD-Q4_K_XL", False, False)["STRATA_ROUTE_TAIL_SKIP"], "0")
|
|
self.assertNotIn("STRATA_ROUTE_TAIL_SKIP", f("unsloth", "UD-Q4_K_XL", False, True)) # budget mode keeps the skip
|
|
|
|
def test_other_families_are_untouched(self):
|
|
for fam, m in (("qwen", "IQ3_S"), ("swift", "Q2_0"), ("coder", "IQ1_M"), ("nvfp4", "NVFP4")):
|
|
self.assertEqual(setup.unsloth_default_env(fam, m, False, False), {}, fam)
|
|
|
|
|
|
class EnginePlaceholders(unittest.TestCase):
|
|
def test_every_engine_minimum_is_this_release(self):
|
|
self.assertEqual(setup.NVFP4_ENGINE, (0, 1, 44))
|
|
self.assertEqual(setup.MIN_ENGINE, (0, 1, 46))
|
|
for m in ("UD-Q5_K_XL", "UD-Q6_K_XL", "NVFP4", "NVFP4-NVIDIA"):
|
|
self.assertEqual(setup.MODELS[m]["engine"], (0, 1, 44), m)
|
|
# No minimum may name an engine newer than this tree (a placeholder for a release that does not exist yet would make setup
|
|
# ask for an engine nobody can download). The tree's version is read from CMakeLists.txt, so this does not go stale each release.
|
|
text = (ROOT / "CMakeLists.txt").read_text(encoding="utf-8")
|
|
tree = tuple(int(n) for n in re.search(r"project\(strata VERSION ([0-9.]+)", text).group(1).split("."))
|
|
for what, minimum in [("MIN_ENGINE", setup.MIN_ENGINE), ("NVFP4_ENGINE", setup.NVFP4_ENGINE)] + [(m, setup.MODELS[m]["engine"]) for m in setup.MODELS if "engine" in setup.MODELS[m]]:
|
|
self.assertLessEqual(tuple(minimum), tree[:len(minimum)], f"{what} {minimum} is ahead of the tree {tree}")
|
|
|
|
|
|
class KqLibrary(unittest.TestCase):
|
|
def test_a_zip_without_strata_kq_does_not_leave_an_old_one(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
eng, tmp = Path(d) / "engine", Path(d) / "new"
|
|
eng.mkdir()
|
|
tmp.mkdir()
|
|
(eng / setup.EXE).write_bytes(b"old")
|
|
(eng / setup.KQ_LIB).write_bytes(b"old kq")
|
|
(tmp / setup.EXE).write_bytes(b"new")
|
|
setup.install_unpacked(tmp, eng)
|
|
self.assertEqual((eng / setup.EXE).read_bytes(), b"new")
|
|
self.assertFalse((eng / setup.KQ_LIB).exists())
|
|
self.assertEqual((eng / setup.PREVIOUS_ENGINE / setup.KQ_LIB).read_bytes(), b"old kq") # a rollback gets it back
|
|
|
|
def test_a_zip_with_strata_kq_replaces_it(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
eng, tmp = Path(d) / "engine", Path(d) / "new"
|
|
eng.mkdir()
|
|
tmp.mkdir()
|
|
(eng / setup.KQ_LIB).write_bytes(b"old kq")
|
|
(tmp / setup.EXE).write_bytes(b"new")
|
|
(tmp / setup.KQ_LIB).write_bytes(b"new kq")
|
|
setup.install_unpacked(tmp, eng)
|
|
self.assertEqual((eng / setup.KQ_LIB).read_bytes(), b"new kq")
|
|
|
|
|
|
class ImageEncoderDefault(unittest.TestCase):
|
|
"""#2406: "images: yes" gets the CPU encoder on an NVIDIA card under 24 GB (the GPU encoder cost 3-5% of text speed on
|
|
16 GB cards, 35-60% on an RTX 4080 SUPER under Windows); --vision gpu is kept with a note."""
|
|
|
|
def test_yes_follows_the_card(self):
|
|
f = setup.flash_vision_encoder
|
|
self.assertEqual(f("yes", {"vram_gb": 12.0}), "cpu")
|
|
self.assertEqual(f("yes", {"vram_gb": 15.9}), "cpu")
|
|
self.assertEqual(f("yes", {"vram_gb": 24.0}), "gpu")
|
|
self.assertEqual(f("yes", {"vram_gb": 32.0}), "gpu")
|
|
self.assertEqual(f("yes", {}), "gpu") # unknown VRAM: the old default
|
|
self.assertEqual(f("cpu", {"vram_gb": 32.0}), "cpu")
|
|
self.assertEqual(f("none", {"vram_gb": 16.0}), "none")
|
|
|
|
def test_gpu_asked_is_kept_with_a_note(self):
|
|
import contextlib, io
|
|
out = io.StringIO()
|
|
with contextlib.redirect_stdout(out):
|
|
self.assertEqual(setup.flash_vision_encoder("gpu", {"vram_gb": 16.0}), "gpu")
|
|
self.assertEqual(setup.flash_vision_encoder("gpu", {"vram_gb": 32.0}), "gpu")
|
|
self.assertEqual(out.getvalue().count("kept as you chose"), 1)
|
|
self.assertIn("#2406", out.getvalue())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|