mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-08 17:35:22 -10:00
Mixed makes of graphics cards: only the cards the engine can drive are offered, never a cross-make split, one plain line when the other make has the bigger card, two models at once said to be unsupported (launch, setup, app card list)
This commit is contained in:
1 parent
d060783fbb
commit
e107297a8f
10 files changed
+366
-26
No files matched your search
@@ -6756,6 +6756,13 @@ def main() -> int:
|
||||
for g in amd_ok)
|
||||
+ f" ({'the ready-made AMD engine, no images' if WIN else 'compiled here, images on the CPU'}"
|
||||
" - 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.")
|
||||
from strata_control import gpu_fit
|
||||
bigger = gpu_fit.other_vendor_note([{**g, "vendor": "nvidia", "usable": gpu_problem(g) is None} for g in found] +
|
||||
[{**g, "vendor": "amd", "usable": True} for g in amd_ok], "nvidia")
|
||||
if bigger:
|
||||
say(" " + bigger)
|
||||
hip = ask("Which cards?", ["1", "2"], "1", a.yes or a.check) == "2"
|
||||
if a.check and not hip:
|
||||
say(f" (the AMD card: {'START-HERE.bat' if WIN else './setup.sh'} --backend hip)")
|
||||
|
||||
@@ -72,6 +72,8 @@ class Ctx:
|
||||
r = next((x for x in self.rows if (x.family, x.model) == (s.family, s.model)), None)
|
||||
if r is not None and r.verdict != "fits":
|
||||
out.append(f"{r.title}: {r.words}.")
|
||||
if self.computer.other_vendor_note:
|
||||
out.append(self.computer.other_vendor_note)
|
||||
missing = acc.missing_helper(s.access)
|
||||
if missing:
|
||||
out.append(missing)
|
||||
|
||||
@@ -117,17 +117,26 @@ class Computer:
|
||||
os: str = ""
|
||||
avx2: bool | None = None
|
||||
raw: dict = field(default_factory=dict)
|
||||
engine_vendor: str | None = None # the make of card the engine drives (nvidia, amd, intel, apple); None: the cards name none
|
||||
|
||||
@property
|
||||
def usable_gpus(self) -> list[dict]:
|
||||
"""The cards that count (strata_control/gpu_fit.py: an integrated chip or a tiny card is ignored), the largest first."""
|
||||
"""The cards that count and the engine can drive (strata_control/gpu_fit.py: an integrated chip, a tiny card or a card of another
|
||||
make is left out), the largest first. One engine drives one make, so a model is never split across makes."""
|
||||
from strata_control import gpu_fit
|
||||
return gpu_fit.usable_cards(self.gpus)[0]
|
||||
return gpu_fit.usable_cards(gpu_fit.for_vendor(self.gpus, self.engine_vendor)[0])[0]
|
||||
|
||||
@property
|
||||
def ignored_gpus(self) -> list[dict]:
|
||||
from strata_control import gpu_fit
|
||||
return gpu_fit.usable_cards(self.gpus)[1]
|
||||
mine, others = gpu_fit.for_vendor(self.gpus, self.engine_vendor)
|
||||
return gpu_fit.usable_cards(mine)[1] + [{**c, "why_ignored": "another make of card than this engine drives"} for c in others]
|
||||
|
||||
@property
|
||||
def other_vendor_note(self) -> str | None:
|
||||
"""One plain line when a card of another make is the bigger one (None otherwise)."""
|
||||
from strata_control import gpu_fit
|
||||
return gpu_fit.other_vendor_note(self.gpus, self.engine_vendor)
|
||||
|
||||
@property
|
||||
def main_vram_gb(self) -> float:
|
||||
@@ -265,7 +274,26 @@ class Catalog:
|
||||
def computer(self, fresh: bool = False) -> Computer:
|
||||
hw = self.hardware(fresh)
|
||||
return Computer(ram_gb=hw.get("ram_gb"), gpus=list(hw.get("gpus") or []), disk_free_gb=hw.get("disk_free_gb"),
|
||||
os=str(hw.get("os") or ""), avx2=(hw.get("cpu") or {}).get("avx2"), raw=hw)
|
||||
os=str(hw.get("os") or ""), avx2=(hw.get("cpu") or {}).get("avx2"), raw=hw,
|
||||
engine_vendor=self.engine_vendor(hw))
|
||||
|
||||
def installed_backend(self) -> str | None:
|
||||
"""The installed engine's backend (cuda, hip, sycl, cpu, metal), None when there is no usable engine yet."""
|
||||
try:
|
||||
from strata_control import engine_component as ec
|
||||
rep = ec.status()
|
||||
if rep.get("usable"):
|
||||
return (rep.get("engine") or {}).get("backend")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return None
|
||||
|
||||
def engine_vendor(self, hw: dict | None = None) -> str | None:
|
||||
"""The make of card the engine drives: the installed engine's; with none installed, the engine setup would fetch (NVIDIA first when the
|
||||
computer has several makes)."""
|
||||
from strata_control import gpu_fit
|
||||
hw = hw if hw is not None else self.hardware()
|
||||
return gpu_fit.pick_vendor(list(hw.get("gpus") or []), self.installed_backend())
|
||||
|
||||
# ---- the tables
|
||||
def tables(self):
|
||||
@@ -280,7 +308,7 @@ class Catalog:
|
||||
|
||||
def rows(self, hw: dict | None = None) -> list[ModelRow]:
|
||||
hw = hw or self.hardware()
|
||||
comp = Computer(ram_gb=hw.get("ram_gb"), gpus=list(hw.get("gpus") or []))
|
||||
comp = Computer(ram_gb=hw.get("ram_gb"), gpus=list(hw.get("gpus") or []), engine_vendor=self.engine_vendor(hw))
|
||||
vram = comp.main_vram_gb
|
||||
rec = self.recommendation(hw)
|
||||
out: list[ModelRow] = []
|
||||
@@ -361,13 +389,9 @@ class Catalog:
|
||||
# ---- pictures
|
||||
def engine_backend(self) -> str | None:
|
||||
"""The installed engine's backend (cuda, hip, sycl, cpu, metal), else the one this computer's graphics card needs, else None."""
|
||||
try:
|
||||
from strata_control import engine_component as ec
|
||||
rep = ec.status()
|
||||
if rep.get("usable"):
|
||||
return (rep.get("engine") or {}).get("backend")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
installed = self.installed_backend()
|
||||
if installed:
|
||||
return installed
|
||||
for g in self.computer().usable_gpus:
|
||||
v = str(g.get("vendor") or "").lower()
|
||||
if g.get("unified_gb") or v == "apple":
|
||||
|
||||
@@ -253,12 +253,39 @@ def apply_defaults(s: S.LaunchSettings, catalog) -> tuple[S.LaunchSettings, list
|
||||
s.api_key = access.new_key()
|
||||
notices.append(S.Notice("warn", "A new API key was made, because the server is shared beyond this computer.",
|
||||
"It is saved with the preset; show it with: strata launch --show-key"))
|
||||
notices += gpu_notices(s, comp)
|
||||
notices += S.check_access(s)
|
||||
vram = comp.main_vram_gb or None
|
||||
notices += S.check_parallel(s, vram_gb=vram)
|
||||
return s, notices
|
||||
|
||||
|
||||
def gpu_notices(s: S.LaunchSettings, comp) -> list[S.Notice]:
|
||||
"""Cards of another make than the engine drives: never offered, never mixed into a split. Said in plain words: a named card of the other
|
||||
make is refused with the reason, and a better card of the other make is mentioned once (recommend, never force)."""
|
||||
from strata_control import gpu_fit
|
||||
out: list[S.Notice] = []
|
||||
vendor = comp.engine_vendor
|
||||
if s.gpus and vendor:
|
||||
mine = {int(g.get("index", -1)) for g in comp.usable_gpus}
|
||||
others = [g for g in gpu_fit.for_vendor(comp.gpus, vendor)[1] if gpu_fit.usable_cards([g])[0]]
|
||||
for idx in s.gpus:
|
||||
if int(idx) in mine:
|
||||
continue
|
||||
other = next((g for g in others if int(g.get("index", -2)) == int(idx)), None)
|
||||
make = gpu_fit.VENDOR_NAME.get(vendor, vendor)
|
||||
if other is not None:
|
||||
omake = gpu_fit.VENDOR_NAME.get(str(other.get("vendor")), str(other.get("vendor")))
|
||||
out.append(S.Notice("error", f"Card {idx} is an {omake} card ({other.get('name')}), and this build of Strata drives {make} cards only. "
|
||||
f"A model cannot be split across {make} and {omake} cards. {gpu_fit.SECOND_MODEL_SENTENCE}",
|
||||
f"Name a {make} card (" + ", ".join(str(g.get("index")) for g in comp.usable_gpus) + f"), or install the {omake} "
|
||||
"build of Strata to use that card."))
|
||||
note = comp.other_vendor_note
|
||||
if note:
|
||||
out.append(S.Notice("warn", note, ""))
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ plain-text run
|
||||
|
||||
def _say(line: str = "") -> None:
|
||||
|
||||
@@ -176,7 +176,7 @@ def cards_for_launch(s: S.LaunchSettings, catalog, ram_gb: float | None = None)
|
||||
return None
|
||||
row = next((r for r in catalog.rows() if (r.family, r.model) == (s.family, s.model)), None)
|
||||
mem = float(row.memory_gb) if row is not None and row.memory_gb else 44.0
|
||||
return gpu_fit.recommended_cards(mem, comp.ram_gb, comp.gpus) or None
|
||||
return gpu_fit.recommended_cards(mem, comp.ram_gb, comp.gpus, comp.engine_vendor) or None
|
||||
except Exception: # noqa: BLE001 - never block a launch over the choice of cards
|
||||
return None
|
||||
|
||||
|
||||
@@ -430,6 +430,9 @@ class ModelScreen(Base):
|
||||
if r.spill_gb:
|
||||
out.append(f"About {r.spill_gb:.0f} GB would be read from the SSD while it answers. ", style=MUTE)
|
||||
out.append("The n-gram table stays on your SSD and is not counted above.", style=MUTE)
|
||||
note = self.ctx.computer.other_vendor_note
|
||||
if note:
|
||||
out.append(" " + note, style=WARN)
|
||||
return out
|
||||
|
||||
def on_cursor(self) -> None:
|
||||
@@ -527,7 +530,7 @@ class CardsScreen(Base):
|
||||
|
||||
def choices(self) -> dict:
|
||||
from strata_control import gpu_fit
|
||||
return gpu_fit.options(self.model_memory(), self.ctx.computer.ram_gb, self.ctx.computer.gpus)
|
||||
return gpu_fit.options(self.model_memory(), self.ctx.computer.ram_gb, self.ctx.computer.gpus, self.ctx.computer.engine_vendor)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.res = self.choices()
|
||||
|
||||
@@ -35,6 +35,75 @@ def _round1(x: float) -> float:
|
||||
return round(float(x) * 10) / 10
|
||||
|
||||
|
||||
# ---- one engine, one vendor -----------------------------------------------------------------------------------------------
|
||||
# Every Strata engine package drives the cards of ONE vendor (CUDA: NVIDIA, HIP: AMD, SYCL: Intel, Metal: Apple). A model is never split
|
||||
# across vendors, and two models at once, one per vendor, is not supported yet. So the cards offered for running and splitting are the
|
||||
# ones the engine can drive; a better card of the other vendor is mentioned in one plain line (recommend, never force).
|
||||
BACKEND_VENDOR = {"cuda": "nvidia", "hip": "amd", "sycl": "intel", "metal": "apple"}
|
||||
VENDOR_NAME = {"nvidia": "NVIDIA", "amd": "AMD", "intel": "Intel", "apple": "Apple"}
|
||||
SECOND_MODEL_SENTENCE = ("Running two models at the same time, one on each make of graphics card, is not supported yet. "
|
||||
"Strata runs one model at a time on the cards of one make.")
|
||||
|
||||
|
||||
def vendor_of_backend(backend: str | None) -> str | None:
|
||||
return BACKEND_VENDOR.get(str(backend or "").lower())
|
||||
|
||||
|
||||
def _vendor(card: dict) -> str | None:
|
||||
v = card.get("vendor")
|
||||
return str(v).lower() if v else None
|
||||
|
||||
|
||||
def pick_vendor(cards: list[dict], backend: str | None = None) -> str | None:
|
||||
"""The make of card the engine drives: the installed (or detected) engine's, else the only make among the cards that count; with more
|
||||
than one make, NVIDIA (the engine setup picks first), else the make of the card with the most memory. None: no card names a make."""
|
||||
named = vendor_of_backend(backend)
|
||||
if named:
|
||||
return named
|
||||
keep, _ = usable_cards(cards)
|
||||
makes = {_vendor(c) for c in keep if _vendor(c)}
|
||||
if len(makes) <= 1:
|
||||
return next(iter(makes), None)
|
||||
if "nvidia" in makes:
|
||||
return "nvidia"
|
||||
return _vendor(keep[0])
|
||||
|
||||
|
||||
def for_vendor(cards: list[dict], vendor: str | None) -> tuple[list[dict], list[dict]]:
|
||||
"""(the cards the engine of that make can drive, the cards of other makes). A card that names no make goes with the engine's."""
|
||||
if not vendor:
|
||||
return list(cards or []), []
|
||||
mine = [c for c in cards or [] if _vendor(c) in (None, vendor)]
|
||||
return mine, [c for c in cards or [] if _vendor(c) not in (None, vendor)]
|
||||
|
||||
|
||||
def _short_name(c: dict, vendor: str) -> str:
|
||||
n = str(c.get("name") or "graphics card")
|
||||
for junk in ("NVIDIA GeForce ", "NVIDIA ", "AMD Radeon ", "AMD ", "Intel "):
|
||||
if n.startswith(junk):
|
||||
n = n[len(junk):]
|
||||
break
|
||||
return f"{VENDOR_NAME.get(vendor, vendor.title())} {n}".strip()
|
||||
|
||||
|
||||
def other_vendor_note(cards: list[dict], vendor: str | None) -> str | None:
|
||||
"""One plain line when the other make's best card is bigger than the best card the engine can use, else None:
|
||||
"This computer also has an AMD R9700 (32 GB). The AMD build of Strata can use it - it fits bigger models." """
|
||||
if not vendor:
|
||||
return None
|
||||
mine, others = for_vendor(usable_cards(cards)[0], vendor)
|
||||
others = [c for c in others if _vendor(c) and not _shares_memory(c)]
|
||||
if not others:
|
||||
return None
|
||||
best = max(others, key=lambda c: float(c.get("vram_gb") or 0))
|
||||
mine_best = max((float(c.get("vram_gb") or 0) for c in mine), default=0.0)
|
||||
if float(best.get("vram_gb") or 0) <= mine_best:
|
||||
return None
|
||||
other = _vendor(best)
|
||||
return (f"This computer also has an {_short_name(best, other)} ({float(best.get('vram_gb') or 0):.0f} GB). "
|
||||
f"The {VENDOR_NAME.get(other, other.title())} build of Strata can use it — it fits bigger models.")
|
||||
|
||||
|
||||
def memory_needed(variant: dict) -> float:
|
||||
"""RAM + VRAM the model asks for (native-fit.ts memoryNeeded)."""
|
||||
if isinstance(variant.get("memory_gb"), (int, float)):
|
||||
@@ -137,15 +206,20 @@ def _option(key: str, cards: list[dict], memory_gb: float, ram_gb: float | None)
|
||||
_RANK = {"fits": 0, "tight": 1, "unknown": 2, "spills": 3}
|
||||
|
||||
|
||||
def options(memory_gb: float, ram_gb: float | None, cards: list[dict]) -> dict[str, Any]:
|
||||
def options(memory_gb: float, ram_gb: float | None, cards: list[dict], vendor: str | None = None) -> dict[str, Any]:
|
||||
"""The choices for this model on this computer: {"options": [...], "recommended": key, "ignored": [...], "cards": n counted}.
|
||||
|
||||
Options (best known first): the largest card alone; with two or more cards that can share a model, the two largest as a layer split
|
||||
(and all of them when there are more than two); then every other card alone. Exactly one option is recommended:
|
||||
the single largest card when the model fits it (with RAM); otherwise the split if it fits better (a smaller verdict or less SSD use);
|
||||
otherwise the single largest card again (nothing better to offer)."""
|
||||
keep, ignored = usable_cards(cards)
|
||||
out: dict[str, Any] = {"options": [], "recommended": None, "ignored": ignored, "cards": len(keep)}
|
||||
vendor = vendor or pick_vendor(cards)
|
||||
mine, others = for_vendor(cards, vendor)
|
||||
keep, ignored = usable_cards(mine)
|
||||
ignored = ignored + [{**c, "why_ignored": f"it needs the {VENDOR_NAME.get(_vendor(c), _vendor(c))} build of Strata, and this engine drives "
|
||||
f"{VENDOR_NAME.get(vendor, vendor)} cards"} for c in others]
|
||||
out: dict[str, Any] = {"options": [], "recommended": None, "ignored": ignored, "cards": len(keep), "vendor": vendor,
|
||||
"other_vendor_note": other_vendor_note(cards, vendor)}
|
||||
if not keep:
|
||||
return out
|
||||
names = [str(c.get("name")) for c in keep]
|
||||
@@ -169,7 +243,7 @@ def options(memory_gb: float, ram_gb: float | None, cards: list[dict]) -> dict[s
|
||||
return out
|
||||
|
||||
|
||||
def recommended_cards(memory_gb: float, ram_gb: float | None, cards: list[dict]) -> list[int]:
|
||||
def recommended_cards(memory_gb: float, ram_gb: float | None, cards: list[dict], vendor: str | None = None) -> list[int]:
|
||||
"""The card numbers of the recommended option (empty: no usable card)."""
|
||||
res = options(memory_gb, ram_gb, cards)
|
||||
res = options(memory_gb, ram_gb, cards, vendor)
|
||||
return next((o["cards"] for o in res["options"] if o["recommended"]), [])
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Owner question 2026-10-18: a PC with an RTX 5060 Ti (16 GB) and an AMD R9700 (32 GB). Every Strata engine drives the cards of ONE make
|
||||
(CUDA: NVIDIA, HIP: AMD), so one model is never split across makes, only the cards the engine can drive are offered, a bigger card of the
|
||||
other make is mentioned in one plain line, and two models at once (one per make) is said to be unsupported."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from strata_cli.launch import cmd, settings as S
|
||||
from strata_cli.launch.runner import cards_for_launch
|
||||
from strata_cli.launch.screens import flow_for
|
||||
from strata_control import gpu_fit as G
|
||||
from tests.terminal.launch_support import HW_12, Home, catalog
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
NV_5060TI = {"index": 0, "name": "NVIDIA GeForce RTX 5060 Ti", "vram_gb": 16.0, "usable": True, "vendor": "nvidia", "arch": "120"}
|
||||
AMD_R9700 = {"index": 0, "name": "AMD Radeon AI PRO R9700", "vram_gb": 32.0, "usable": True, "vendor": "amd", "arch": "gfx1201"}
|
||||
NV_2080TI = {"index": 0, "name": "NVIDIA GeForce RTX 2080 Ti", "vram_gb": 11.0, "usable": True, "vendor": "nvidia", "arch": "75"}
|
||||
NV_5070 = {"index": 1, "name": "NVIDIA GeForce RTX 5070", "vram_gb": 12.0, "usable": True, "vendor": "nvidia", "arch": "120"}
|
||||
NV_5070_ALONE = {**NV_5070, "index": 0}
|
||||
|
||||
NOTE_AMD = "This computer also has an AMD AI PRO R9700 (32 GB). The AMD build of Strata can use it — it fits bigger models."
|
||||
|
||||
|
||||
class GpuFitMixes(unittest.TestCase):
|
||||
def test_nvidia_engine_offers_only_the_nvidia_card_and_says_the_amd_one_is_bigger(self):
|
||||
r = G.options(44.0, 62.0, [NV_5060TI, AMD_R9700], "nvidia")
|
||||
self.assertEqual([o["key"] for o in r["options"]], ["single"])
|
||||
self.assertEqual(r["options"][0]["label"], "RTX 5060 Ti + RAM")
|
||||
self.assertNotIn("R9700", r["options"][0]["label"])
|
||||
self.assertEqual(r["other_vendor_note"], NOTE_AMD)
|
||||
self.assertEqual(r["vendor"], "nvidia")
|
||||
self.assertEqual(len(r["ignored"]), 1)
|
||||
self.assertIn("AMD build of Strata", r["ignored"][0]["why_ignored"] + "AMD build of Strata")
|
||||
self.assertIn("needs the AMD build", r["ignored"][0]["why_ignored"])
|
||||
|
||||
def test_amd_engine_offers_only_the_r9700_and_has_no_note_because_the_nvidia_card_is_smaller(self):
|
||||
r = G.options(44.0, 62.0, [NV_5060TI, AMD_R9700], "amd")
|
||||
self.assertEqual([o["key"] for o in r["options"]], ["single"])
|
||||
self.assertIn("R9700", r["options"][0]["label"])
|
||||
self.assertIsNone(r["other_vendor_note"])
|
||||
|
||||
def test_never_a_cross_vendor_split_even_with_two_cards_that_could_share(self):
|
||||
cards = [NV_5060TI, {**AMD_R9700, "index": 1}, {**NV_5060TI, "index": 2}]
|
||||
for vendor in ("nvidia", "amd"):
|
||||
for o in G.options(70.0, 32.0, cards, vendor)["options"]:
|
||||
makes = {next(c["vendor"] for c in cards if c["index"] == i and c["vendor"] == vendor) for i in o["cards"]}
|
||||
self.assertEqual(makes, {vendor}, o["key"])
|
||||
|
||||
def test_without_an_engine_named_the_default_is_nvidia_first(self):
|
||||
self.assertEqual(G.pick_vendor([NV_5060TI, AMD_R9700]), "nvidia")
|
||||
self.assertEqual(G.pick_vendor([AMD_R9700]), "amd")
|
||||
self.assertEqual(G.pick_vendor([NV_5060TI, AMD_R9700], "hip"), "amd") # the installed engine decides
|
||||
self.assertEqual(G.pick_vendor([NV_5060TI, AMD_R9700], "cuda"), "nvidia")
|
||||
|
||||
def test_a_card_that_names_no_make_is_left_alone(self):
|
||||
r = G.options(44.0, 62.0, [{"index": 0, "name": "A", "vram_gb": 16, "usable": True}, {"index": 1, "name": "B", "vram_gb": 16, "usable": True}])
|
||||
self.assertEqual([o["key"] for o in r["options"]][:2], ["single", "split"])
|
||||
self.assertIsNone(r["other_vendor_note"])
|
||||
|
||||
def test_2080ti_and_5070_are_one_make_so_the_split_is_offered_with_the_newer_card_leading_by_memory(self):
|
||||
r = G.options(44.0, 32.0, [NV_2080TI, NV_5070])
|
||||
self.assertEqual([o["key"] for o in r["options"]][:2], ["single", "split"])
|
||||
self.assertEqual(r["options"][0]["cards"], [1]) # the 12 GB RTX 5070 is the larger card
|
||||
self.assertEqual(r["options"][1]["cards"], [1, 0])
|
||||
self.assertIn("RTX 5070 + RTX 2080 Ti + RAM (layer split)", r["options"][1]["label"])
|
||||
self.assertIsNone(r["other_vendor_note"])
|
||||
self.assertEqual(r["ignored"], [])
|
||||
|
||||
def test_2080ti_5070_with_an_amd_card_beside_still_never_mixes(self):
|
||||
r = G.options(44.0, 32.0, [NV_2080TI, NV_5070, {**AMD_R9700, "index": 2}], "nvidia")
|
||||
for o in r["options"]:
|
||||
self.assertNotIn(2, o["cards"])
|
||||
self.assertEqual(r["other_vendor_note"], NOTE_AMD)
|
||||
|
||||
def test_the_two_models_sentence_is_plain(self):
|
||||
self.assertIn("not supported yet", G.SECOND_MODEL_SENTENCE)
|
||||
self.assertIn("one model at a time", G.SECOND_MODEL_SENTENCE)
|
||||
|
||||
|
||||
class LaunchMix(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.home = Home().__enter__()
|
||||
self.addCleanup(self.home.__exit__, None, None, None)
|
||||
self.hw = {**HW_12, "gpus": [NV_5060TI, AMD_R9700]}
|
||||
p = mock.patch("strata_cli.launch.catalog.Catalog.installed_backend", return_value=None)
|
||||
p.start()
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
def test_the_computer_counts_only_the_cards_the_engine_drives(self):
|
||||
comp = catalog(self.hw).computer()
|
||||
self.assertEqual(comp.engine_vendor, "nvidia")
|
||||
self.assertEqual([g["name"] for g in comp.usable_gpus], ["NVIDIA GeForce RTX 5060 Ti"])
|
||||
self.assertEqual(comp.main_vram_gb, 16.0) # not the R9700's 32
|
||||
self.assertEqual(comp.other_vendor_note, NOTE_AMD)
|
||||
self.assertEqual(len(flow_for(type("C", (), {"computer": comp}))), 9) # no Cards step: one card the engine can drive
|
||||
|
||||
def test_with_the_amd_engine_installed_the_r9700_counts_and_the_note_goes(self):
|
||||
with mock.patch("strata_cli.launch.catalog.Catalog.installed_backend", return_value="hip"):
|
||||
comp = catalog(self.hw).computer()
|
||||
self.assertEqual(comp.engine_vendor, "amd")
|
||||
self.assertEqual(comp.main_vram_gb, 32.0)
|
||||
self.assertIsNone(comp.other_vendor_note)
|
||||
|
||||
def test_the_wizard_says_it_in_one_line(self):
|
||||
s, notes = cmd.apply_defaults(S.LaunchSettings(family="qwen", model="IQ3_XXS"), catalog(self.hw))
|
||||
self.assertIn(NOTE_AMD, [n.line() for n in notes] + [n.text for n in notes])
|
||||
|
||||
def test_naming_the_amd_card_for_the_nvidia_engine_is_refused_in_plain_words(self):
|
||||
# NVIDIA has cards 0 and 1 here; AMD's card is number 2 in this mock
|
||||
hw = {**HW_12, "gpus": [NV_5060TI, {**NV_5060TI, "index": 1}, {**AMD_R9700, "index": 2}]}
|
||||
s, notes = cmd.apply_defaults(S.LaunchSettings(family="qwen", model="IQ3_XXS", gpus=[0, 2]), catalog(hw))
|
||||
errors = [n for n in notes if n.level == "error"]
|
||||
self.assertEqual(len(errors), 1)
|
||||
text = errors[0].text
|
||||
self.assertIn("Card 2 is an AMD card", text)
|
||||
self.assertIn("cannot be split across NVIDIA and AMD cards", text)
|
||||
self.assertIn("one model at a time", text)
|
||||
self.assertIn("install the AMD build of Strata", errors[0].next)
|
||||
|
||||
def test_cards_for_launch_never_returns_the_other_make(self):
|
||||
hw = {**HW_12, "ram_gb": 32.0, "gpus": [NV_5060TI, {**NV_5060TI, "index": 1}, {**AMD_R9700, "index": 2}]}
|
||||
cat = catalog(hw)
|
||||
got = cards_for_launch(S.LaunchSettings(family="qwen", model="IQ3_XXS"), cat)
|
||||
self.assertTrue(all(i in (0, 1) for i in (got or [])))
|
||||
|
||||
def test_the_app_catalog_uses_the_engine_make(self):
|
||||
cat = catalog(self.hw)
|
||||
mcp = cat.mcp
|
||||
hw = dict(self.hw)
|
||||
mcp.Strata._engine_vendor(hw)
|
||||
self.assertEqual(hw["engine_vendor"], "nvidia")
|
||||
self.assertEqual(hw["gpu_note"], NOTE_AMD)
|
||||
usable = cat.strata.engine_gpus(hw)
|
||||
self.assertEqual([g["name"] for g in usable], ["NVIDIA GeForce RTX 5060 Ti"])
|
||||
self.assertEqual(cat.strata.gpu_options({"ram_gb": 20, "arena_gb": 30, "download_gb": 30}, 62.0, hw),
|
||||
{"options": [], "recommended": None, "other_vendor_note": NOTE_AMD})
|
||||
two = {**hw, "gpus": [NV_5060TI, {**NV_5060TI, "index": 1}, AMD_R9700]}
|
||||
res = cat.strata.gpu_options({"ram_gb": 20, "arena_gb": 30, "download_gb": 30}, 62.0, two)
|
||||
self.assertEqual({tuple(o["cards"]) for o in res["options"]}, {(0,), (0, 1), (1,)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+42
-7
@@ -767,11 +767,41 @@ class Strata:
|
||||
if g.get("uma") and g.get("usable"):
|
||||
g["usable"], g["problem"] = False, "integrated graphics: Strata uses the NVIDIA card in this computer"
|
||||
hw["gpus"] = nv + amd
|
||||
self._engine_vendor(hw)
|
||||
if WIN:
|
||||
hw["display_adapters"] = windows_video_controllers()
|
||||
hw["nvidia_smi"] = bool(shutil.which("nvidia-smi"))
|
||||
return hw
|
||||
|
||||
@staticmethod
|
||||
def _engine_vendor(hw: dict) -> None:
|
||||
"""One engine drives the cards of ONE make (CUDA: NVIDIA, HIP: AMD, ...): note which one for this PC (the installed engine's, else the one
|
||||
setup fetches first) and, when the other make has the bigger card, the one plain line that says so. The model list below only counts
|
||||
the cards this engine can drive; a model is never split across makes (strata_control/gpu_fit.py)."""
|
||||
try:
|
||||
from strata_control import gpu_fit
|
||||
backend = None
|
||||
try:
|
||||
from strata_control import engine_component as ec
|
||||
rep = ec.status()
|
||||
backend = (rep.get("engine") or {}).get("backend") if rep.get("usable") else None
|
||||
except Exception: # noqa: BLE001
|
||||
backend = None
|
||||
vendor = gpu_fit.pick_vendor(hw.get("gpus", []), backend)
|
||||
hw["engine_vendor"] = vendor
|
||||
note = gpu_fit.other_vendor_note(hw.get("gpus", []), vendor)
|
||||
if note:
|
||||
hw["gpu_note"] = note
|
||||
except Exception: # noqa: BLE001 - never break the hardware page over this
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def engine_gpus(hw: dict | None) -> list:
|
||||
"""The usable cards the engine of this PC drives (all usable cards when the make is not known)."""
|
||||
hw = hw or {}
|
||||
vendor = hw.get("engine_vendor")
|
||||
return [g for g in hw.get("gpus", []) if g.get("usable") and (not vendor or not g.get("vendor") or g.get("vendor") == vendor)]
|
||||
|
||||
# ---- a Mac (Apple Silicon): unified memory, the Metal engine. The rules are strata_control/mac_fit.py's (the same module
|
||||
# metal/setup_mac.py uses), so the app, the Terminal and a Mac that builds the engine itself judge a model the same way.
|
||||
def mac(self):
|
||||
@@ -867,7 +897,7 @@ class Strata:
|
||||
if hw.get("mac"):
|
||||
return self._mac_recommend(hw, models, families)
|
||||
S = self.setup_module()
|
||||
usable = [g for g in hw.get("gpus", []) if g.get("usable")]
|
||||
usable = self.engine_gpus(hw)
|
||||
ram = hw.get("ram_gb") or 0
|
||||
if not usable:
|
||||
why = ("no NVIDIA RTX 20-series-or-newer GPU found (nvidia-smi did not list one)"
|
||||
@@ -927,7 +957,7 @@ class Strata:
|
||||
models, families, _ctx, _src = self.tables()
|
||||
S = self.setup_module()
|
||||
ram = (hw or {}).get("ram_gb")
|
||||
usable = [g for g in (hw or {}).get("gpus", []) if g.get("usable")]
|
||||
usable = self.engine_gpus(hw)
|
||||
vram = max((g["vram_gb"] for g in usable), default=0.0)
|
||||
have = {c.stem[len("strata-"):] for c in self.configs()}
|
||||
out = []
|
||||
@@ -979,8 +1009,11 @@ class Strata:
|
||||
try:
|
||||
from strata_control import gpu_fit
|
||||
mem = float(d["ram_gb"]) if d.get("mem_total") else float(d.get("arena_gb") or d["ram_gb"]) + 1.0
|
||||
res = gpu_fit.options(round(mem, 1), ram, (hw or {}).get("gpus", []))
|
||||
return res if len(res["options"]) >= 2 else None
|
||||
res = gpu_fit.options(round(mem, 1), ram, (hw or {}).get("gpus", []), (hw or {}).get("engine_vendor"))
|
||||
if len(res["options"]) >= 2:
|
||||
return res
|
||||
# one card the engine can drive: no choice to offer, but a bigger card of the other make is still worth one plain line
|
||||
return {"options": [], "recommended": None, "other_vendor_note": res.get("other_vendor_note")} if res.get("other_vendor_note") else None
|
||||
except Exception: # noqa: BLE001 - the choice is a convenience: never break the model list
|
||||
return None
|
||||
|
||||
@@ -1417,8 +1450,10 @@ class Tools:
|
||||
"rope scaling (experimental)",
|
||||
"recommendation": rec, "source": src,
|
||||
"this_pc": {"ram_gb": hw.get("ram_gb"),
|
||||
"gpus": [{k: g.get(k) for k in ("index", "name", "vram_gb", "usable")}
|
||||
for g in hw.get("gpus", [])]}}
|
||||
"gpus": [{k: g.get(k) for k in ("index", "name", "vram_gb", "usable", "vendor")}
|
||||
for g in hw.get("gpus", [])],
|
||||
**({"engine_vendor": hw["engine_vendor"]} if hw.get("engine_vendor") else {}),
|
||||
**({"gpu_note": hw["gpu_note"]} if hw.get("gpu_note") else {})}}
|
||||
|
||||
# ---- strata_remove
|
||||
def strata_remove(self, model=None, confirm=False) -> dict:
|
||||
@@ -1564,7 +1599,7 @@ class Tools:
|
||||
gpu = gpus = kv = low_ram = None
|
||||
context = context or (s.mac().recommend_context(hw.get("ram_gb") or 0) if s.mac() else 32768)
|
||||
if context is None:
|
||||
usable = [g for g in hw.get("gpus", []) if g.get("usable")]
|
||||
usable = s.engine_gpus(hw)
|
||||
vram = max((g["vram_gb"] for g in usable), default=12)
|
||||
context = 32768 if vram < 14 else 65536 if vram < 20 else 131072
|
||||
if models[model].get("budget") and (not hasattr(s.setup_module(), "unsloth_budget") or
|
||||
|
||||
@@ -59,6 +59,25 @@ class SetupFitRule(unittest.TestCase):
|
||||
code, out, cfg, _ = run(62.0, found, "IQ3_XXS", extra=("--gpu", "1"))
|
||||
self.assertEqual(cfg["gpu"], 1)
|
||||
|
||||
def test_a_bigger_amd_card_beside_the_nvidia_one_is_said_in_one_line_and_never_mixed(self):
|
||||
"""RTX 5060 Ti (16 GB) + AMD R9700 (32 GB): the question offers the two makes apart, says one engine drives one make and that two
|
||||
models at once are not supported, and names the bigger AMD card; the NVIDIA answer runs on the NVIDIA card alone."""
|
||||
from unittest import mock
|
||||
found = [card(0, "NVIDIA GeForce RTX 5060 Ti", 16.0, "120")]
|
||||
amd = [{"index": 0, "name": "AMD Radeon AI PRO R9700", "vram_gb": 32.0, "arch": "gfx1201", "vendor": "amd"}]
|
||||
patches = [mock.patch.object(setup, "amd_gpus", lambda *a, **k: [dict(g) for g in amd]),
|
||||
mock.patch.object(setup, "amd_problem", lambda g: None)]
|
||||
argv = ["--family", "qwen", "--no-start", "--model", "IQ3_XXS", "--context", "32768"]
|
||||
code, out, cfg, asked = install(62.0, found, argv, answers={"Which cards?": "1"}, extra=patches)
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("This PC has NVIDIA and AMD cards Strata can use", out)
|
||||
self.assertIn("never split across NVIDIA and AMD cards", out)
|
||||
self.assertIn("two models at the same", out)
|
||||
self.assertIn("not supported yet", out)
|
||||
self.assertIn("This computer also has an AMD AI PRO R9700 (32 GB). The AMD build of Strata can use it", out)
|
||||
self.assertNotIn("Which GPUs?", " ".join(asked)) # one NVIDIA card: no split question
|
||||
self.assertEqual(cfg.get("gpu", 0), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user