mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 06:15:58 -10:00
Two GTX 1080s are a working PC: an 8 GB card starts Off only beside a bigger card, and a PC whose only NVIDIA cards are Pascal uses them on the CUDA 12 engine (#3655)
recommend() (Terminal, app and shared vectors) now turns a card with 8 GB or less Off only when a bigger usable card is there or it has under 4 GB. The hardware report (tools/strata_mcp.py) applies setup's own opt-in rule for a PC with only Pascal/Volta NVIDIA cards and no usable AMD card, so those cards are no longer flagged 'older than the RTX 20 series' (engine_variant cuda12); the feed already picks the cuda12 package for sm_61 and refuses cuda13. Tests: shared gpu-roles vectors, strata_mcp hardware, feed selection for sm_61, and Tune keeping a two-card role map.
This commit is contained in:
1 parent
d9042a16bd
commit
0351ecb11d
9 files changed
+104
-7
No files matched your search
@@ -89,17 +89,20 @@ export function pickMain(cs: RoleCard[]): { main: RoleCard | null; why: MainWhy
|
||||
export type OffWhy = 'cannot_run' | 'igpu' | 'small' | 'helper';
|
||||
export interface Recommendation { roles: Roles; main: number | null; why: MainWhy; reasons: Record<number, MainWhy | OffWhy> }
|
||||
|
||||
/** The best card is Main; every other card starts as Helper, EXCEPT a card with 8 GB or less, built-in graphics beside a real card, and a card the engine cannot run: those start Off. */
|
||||
/** The best card is Main; every other card starts as Helper, EXCEPT a card with 8 GB or less when a bigger card is there (or it has under 4 GB), built-in graphics beside a real card, and a card the engine cannot run: those start Off. */
|
||||
export function recommend(cards: RoleCard[] | null | undefined, vendor?: string | null): Recommendation {
|
||||
const cs = rows(cards, vendor);
|
||||
const { main, why } = pickMain(cs);
|
||||
const roles: Roles = {}, reasons: Recommendation['reasons'] = {};
|
||||
// An 8 GB card is a poor Helper beside a bigger card, but a perfectly good one when 8 GB is all there is: two GTX 1080s are a working
|
||||
// 16 GB pool (#3655). A card with 8 GB or less starts Off only when a bigger usable card is there, or when it is too small to help.
|
||||
const hasBigger = cs.some(c => canRun(c) && !shares(c) && rnd(mem(c)) > OFF_AT_OR_BELOW_GB);
|
||||
for (const c of cs) {
|
||||
const i = c.index;
|
||||
if (main && c === main) { roles[i] = 'main'; reasons[i] = why; }
|
||||
else if (!canRun(c)) { roles[i] = 'off'; reasons[i] = 'cannot_run'; }
|
||||
else if (shares(c) && main) { roles[i] = 'off'; reasons[i] = 'igpu'; }
|
||||
else if (rnd(mem(c)) <= OFF_AT_OR_BELOW_GB) { roles[i] = 'off'; reasons[i] = 'small'; }
|
||||
else if (rnd(mem(c)) <= OFF_AT_OR_BELOW_GB && (hasBigger || mem(c) < MIN_CARD_GB)) { roles[i] = 'off'; reasons[i] = 'small'; }
|
||||
else { roles[i] = 'helper'; reasons[i] = 'helper'; }
|
||||
}
|
||||
return { roles, main: main ? main.index : null, why, reasons };
|
||||
|
||||
@@ -74,3 +74,22 @@ test('roles that make no sense are refused with a plain line', async () => {
|
||||
assert.match(nope.error, /detected graphics cards/);
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('"Tune this model" keeps a two-card role map exactly as the person set it, in the file and in the engine it starts (#3655)', async () => {
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const path = join(f.repo, 'strata-iq2_xs.json');
|
||||
const config = JSON.parse(readFileSync(path, 'utf8'));
|
||||
Object.assign(config, { gpu: [0, 1], gpu_order: 'as_given', gpu_roles: { 0: 'helper', 1: 'main' }, gpu_cards: { 0: 'GTX 1080', 1: 'GTX 1080' } });
|
||||
writeFileSync(path, JSON.stringify(config));
|
||||
await f.control.refresh(true);
|
||||
const tuned = await finishJob(f.control, f.control.operation('tune', { model: 'iq2_xs', profile: 'performance', calibrate: false }).id);
|
||||
assert.equal(tuned.state, 'done', tuned.error);
|
||||
const after = JSON.parse(readFileSync(path, 'utf8'));
|
||||
for (const key of ['gpu', 'gpu_order', 'gpu_roles', 'gpu_cards']) assert.deepEqual(after[key], config[key], `${key} in the settings file is untouched by Tune`);
|
||||
const started = written(f, tuned);
|
||||
assert.deepEqual(started.gpu, [0, 1], 'and the engine Tune loads runs on both cards');
|
||||
assert.deepEqual(started.gpu_roles, { 0: 'helper', 1: 'main' });
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
+2
-2
@@ -57,7 +57,7 @@ the picture reader. (Older versions of this page said "the first is the main one
|
||||
**Recommended roles** (the same rule in the app, `strata launch` and setup; `strata_control/gpu_roles.py`): the faster card
|
||||
is Main (speed is multiprocessors x max clock, the engine's own measure; cards within 5% are the same card, and the one with
|
||||
more memory wins, then the lower number; two identical cards: the highest number is Main, so the engine order stays 0, 1 as it always was; a card with clearly more memory beats a faster one that is at least half as fast).
|
||||
Every other card starts as Helper, except a card with **8 GB or less** (Off), built-in graphics beside a card with memory of
|
||||
Every other card starts as Helper, except a card with **8 GB or less** when a bigger card is there (Off; with only 8 GB cards, say two GTX 1080, the second one is a Helper), built-in graphics beside a card with memory of
|
||||
its own (Off) and a card the engine cannot run (Off). Every role is one click (the app), one key (`strata launch`: M, H, O;
|
||||
R goes back to the recommendation) or one line (below).
|
||||
|
||||
@@ -89,7 +89,7 @@ as Main = the last card, Helper = the first, the rest Off. The recommendation it
|
||||
recommended again; a card you add after choosing roles starts Off. Docker: `GPU_ROLES=1=main,0=helper` is passed as `--gpu-roles`.
|
||||
|
||||
**One rule for which cards count** (`strata_control/gpu_fit.py` keeps the memory arithmetic): not an integrated chip beside
|
||||
a real card, at least 4 GB of graphics memory to be listed, 8 GB or less starts Off. The arithmetic is the app's `fitOn`;
|
||||
a real card, at least 4 GB of graphics memory to be listed, 8 GB or less starts Off beside a bigger card. The arithmetic is the app's `fitOn`;
|
||||
each extra card of a split costs an estimated 2 GB of its own buffers.
|
||||
|
||||
**Choosing the cards without roles** (at setup or at any start):
|
||||
|
||||
@@ -1663,7 +1663,7 @@ def choose_roles(a, found) -> list:
|
||||
roles = dict(gpu_roles.recommend(cards)["roles"])
|
||||
say()
|
||||
say(" Strata recommends a role for each graphics card. Main does the last steps of every answer; a Helper holds part of the model;")
|
||||
say(" Off leaves the card alone (a card with 8 GB or less starts Off). Details: docs/MULTI_GPU.md.")
|
||||
say(" Off leaves the card alone (a card with 8 GB or less starts Off beside a bigger card). Details: docs/MULTI_GPU.md.")
|
||||
by = {c["index"]: c for c in cards}
|
||||
for i in sorted(roles):
|
||||
say(f" GPU {i + 1} {gpu_roles.short_name(by[i]):<22} {by[i]['vram_gb']:>4.0f} GB {roles[i].capitalize()}")
|
||||
|
||||
@@ -107,12 +107,15 @@ def pick_main(cs: list[dict]) -> tuple[dict | None, str]:
|
||||
|
||||
def recommend(cards: list[dict], vendor: str | None = None) -> dict[str, Any]:
|
||||
"""The recommended roles: {"roles": {index: role}, "main": index|None, "why": code, "reasons": {index: code}}.
|
||||
The best card is Main, every other card starts as Helper, EXCEPT a card with 8 GB or less, an integrated chip beside a card with memory of
|
||||
The best card is Main, every other card starts as Helper, EXCEPT a card with 8 GB or less when a bigger card is there (or it has under 4 GB), an integrated chip beside a card with memory of
|
||||
its own, and a card the engine cannot run: those start Off."""
|
||||
cs = rows(cards, vendor)
|
||||
main, why = pick_main(cs)
|
||||
roles: dict[int, str] = {}
|
||||
reasons: dict[int, str] = {}
|
||||
# An 8 GB card is a poor Helper beside a bigger card, but a perfectly good one when 8 GB is all there is: two GTX 1080s are a working
|
||||
# 16 GB pool (#3655). A card with 8 GB or less starts Off only when a bigger usable card is there, or when it is too small to help.
|
||||
has_bigger = any(_can_run(c) and not _shares(c) and round(_mem(c)) > OFF_AT_OR_BELOW_GB for c in cs)
|
||||
for c in cs:
|
||||
i = _idx(c)
|
||||
if main is not None and c is main:
|
||||
@@ -121,7 +124,7 @@ def recommend(cards: list[dict], vendor: str | None = None) -> dict[str, Any]:
|
||||
roles[i], reasons[i] = "off", "cannot_run"
|
||||
elif _shares(c) and main is not None:
|
||||
roles[i], reasons[i] = "off", "igpu"
|
||||
elif round(_mem(c)) <= OFF_AT_OR_BELOW_GB:
|
||||
elif round(_mem(c)) <= OFF_AT_OR_BELOW_GB and (has_bigger or _mem(c) < gpu_fit.MIN_CARD_GB):
|
||||
roles[i], reasons[i] = "off", "small"
|
||||
else:
|
||||
roles[i], reasons[i] = "helper", "helper"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"igpu_only": [{"index": 0, "name": "AMD Radeon 780M", "vram_gb": 0, "integrated": true, "speed": 12}],
|
||||
"bigger_slower": [{"index": 0, "name": "NVIDIA GeForce RTX 3090", "vram_gb": 24, "speed": 90}, {"index": 1, "name": "NVIDIA GeForce RTX 4070", "vram_gb": 12, "speed": 100}],
|
||||
"small_second": [{"index": 0, "name": "NVIDIA GeForce RTX 4070 Ti", "vram_gb": 12, "speed": 100}, {"index": 1, "name": "NVIDIA GeForce RTX 3070", "vram_gb": 8, "speed": 70}],
|
||||
"two_8gb": [{"index": 0, "name": "NVIDIA GeForce GTX 1080", "vram_gb": 8, "speed": 50}, {"index": 1, "name": "NVIDIA GeForce GTX 1080", "vram_gb": 8, "speed": 50}],
|
||||
"tiny_second": [{"index": 0, "name": "NVIDIA GeForce GTX 1080", "vram_gb": 8, "speed": 50}, {"index": 1, "name": "NVIDIA GeForce GT 1030", "vram_gb": 2, "speed": 10}],
|
||||
"cannot": [{"index": 0, "name": "NVIDIA GeForce GTX 1060", "vram_gb": 6, "usable": false, "problem": "too old"}, {"index": 1, "name": "NVIDIA GeForce RTX 4070 Ti", "vram_gb": 12, "speed": 100}],
|
||||
"nospeed": [{"index": 0, "name": "NVIDIA GeForce RTX 3060", "vram_gb": 12}, {"index": 1, "name": "NVIDIA GeForce RTX 3090", "vram_gb": 24}]
|
||||
},
|
||||
@@ -21,6 +23,8 @@
|
||||
{"hw": "igpu_only", "roles": {"0": "main"}, "main": 0, "why": "only"},
|
||||
{"hw": "bigger_slower", "roles": {"0": "main", "1": "helper"}, "main": 0, "why": "memory"},
|
||||
{"hw": "small_second", "roles": {"0": "main", "1": "off"}, "main": 0, "why": "faster"},
|
||||
{"hw": "two_8gb", "roles": {"0": "helper", "1": "main"}, "main": 1, "why": "same"},
|
||||
{"hw": "tiny_second", "roles": {"0": "main", "1": "off"}, "main": 0, "why": "only"},
|
||||
{"hw": "cannot", "roles": {"0": "off", "1": "main"}, "main": 1, "why": "only"},
|
||||
{"hw": "nospeed", "roles": {"0": "helper", "1": "main"}, "main": 1, "why": "memory"}
|
||||
],
|
||||
|
||||
@@ -147,6 +147,28 @@ class Decision(unittest.TestCase):
|
||||
self.assertIn("another make", str(caught.exception))
|
||||
|
||||
|
||||
class Pascal(unittest.TestCase):
|
||||
"""#3655: a GTX 1080 (sm_61) is a working machine on the CUDA 12 engine; the CUDA 13 build has no code for it and is never chosen."""
|
||||
|
||||
FEED = [package("cuda", "cuda13", ("sm_75", "sm_86", "sm_89", "sm_120")),
|
||||
feedmod._engine_package({"version": "0.1.47", "platform": WIN, "backend": "cuda", "variant": "cuda12", "min_driver": {"nvidia": "525.60"},
|
||||
"archs": ["sm_60", "sm_61", "sm_70", "sm_75", "sm_80", "sm_86", "sm_89"], "min_runtime": None, "build_id": "b-12",
|
||||
"format": "zip", "url": "e-12.zip", "size": 10, "sha256": "a" * 64, "manifest_sha256": "b" * 64,
|
||||
"installed_bytes": 20, "notes": ""})]
|
||||
|
||||
def hardware(self, archs):
|
||||
return {"platform": WIN, "backend": "cuda", "runtime": None, "nvidia_driver": "581.15", "cuda_archs": archs, "gfx": [], "gpu_names": []}
|
||||
|
||||
def test_two_gtx_1080_get_the_cuda_12_engine(self):
|
||||
best, reasons = feedmod.select_engine(self.FEED, self.hardware(["sm_61", "sm_61"]))
|
||||
self.assertEqual(best.variant, "cuda12")
|
||||
self.assertTrue(any("cuda13" in r or "sm_75" in r for r in reasons), "the CUDA 13 build is refused for the card")
|
||||
|
||||
def test_a_newer_card_still_gets_the_newest_build(self):
|
||||
best, _ = feedmod.select_engine(self.FEED, self.hardware(["sm_86"]))
|
||||
self.assertEqual(best.variant, "cuda13")
|
||||
|
||||
|
||||
class Host(unittest.TestCase):
|
||||
def test_the_app_shows_the_sentence_not_the_no_engine_for_this_card_text(self):
|
||||
text = (Path(__file__).resolve().parents[2] / "apps" / "host" / "server" / "updates.mjs").read_text(encoding="utf-8")
|
||||
|
||||
@@ -922,6 +922,16 @@ class Strata:
|
||||
amd = []
|
||||
if not WIN and not amd and shutil.which("rocm-smi"):
|
||||
amd = rocm_smi_gpus()
|
||||
# A PC whose only NVIDIA cards are Pascal / Volta (two GTX 1080s) and that has no AMD card Strata can use: setup runs them with the
|
||||
# CUDA 12 engine (old_gpus_opt_in), so the app must not call it "no supported graphics card" and start both cards Off (#3655).
|
||||
# A PC with a newer card keeps recommending that one; naming an old card is still the opt-in.
|
||||
try:
|
||||
if S and nv and S.old_gpus_opt_in(nv, (), None, any(g.get("usable") for g in amd)):
|
||||
for g in nv:
|
||||
if S.sm60_card(g["arch"]) and g.get("problem") and "older than the RTX 20" in str(g["problem"]):
|
||||
g["problem"], g["usable"], g["engine_variant"] = None, True, "cuda12"
|
||||
except Exception: # noqa: BLE001 - the hardware page never breaks over this
|
||||
pass
|
||||
if any(g.get("usable") for g in nv):
|
||||
for g in amd: # a Ryzen's integrated graphics beside an NVIDIA card: the engine runs on the NVIDIA card
|
||||
if g.get("uma") and g.get("usable"):
|
||||
|
||||
@@ -817,6 +817,42 @@ class MixedGraphics(unittest.TestCase):
|
||||
with mock.patch.object(M.sys, "platform", "linux"), mock.patch.object(M, "WIN", False), mock.patch.object(S, "gpus", lambda: []), mock.patch.object(S, "amd_gpus", lambda: [dict(g) for g in igpu]), mock.patch.object(S, "amd_problem", lambda g: None):
|
||||
self.assertTrue(s._hardware()["gpus"][0]["usable"])
|
||||
|
||||
def test_two_gtx_1080_are_a_working_pc_on_the_cuda_12_engine(self):
|
||||
"""#3655: Pascal cards were flagged 'not supported' and both started Off, so the app said no card was usable. When Pascal is all the PC has
|
||||
(and no AMD card), setup runs them with the CUDA 12 engine, and the hardware report now says so; a newer card keeps the old behaviour."""
|
||||
s = M.Strata(HERE.parent)
|
||||
S = s.setup_module()
|
||||
pascal = [{"index": 0, "name": "NVIDIA GeForce GTX 1080", "vram_gb": 8.0, "arch": "61", "driver": "581.15"},
|
||||
{"index": 1, "name": "NVIDIA GeForce GTX 1080", "vram_gb": 8.0, "arch": "61", "driver": "581.15"}]
|
||||
igpu = [{"index": 0, "name": "AMD Radeon(TM) Graphics", "vram_gb": 0.5, "arch": "gfx90c", "uma": True}]
|
||||
patches = (mock.patch.object(M.sys, "platform", "linux"), mock.patch.object(M, "WIN", False),
|
||||
mock.patch.object(S, "amd_gpus", lambda: [dict(g) for g in igpu]), mock.patch.object(S, "amd_problem", lambda g: "integrated graphics"),
|
||||
mock.patch.dict(M.os.environ, {"STRATA_EXPERIMENTAL_SM60": ""}), mock.patch.object(S, "OLD_GPUS", None))
|
||||
with mock.patch.object(S, "gpus", lambda: [dict(g) for g in pascal]):
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
hw = s._hardware()
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
nv = [g for g in hw["gpus"] if g["vendor"] == "nvidia"]
|
||||
self.assertEqual([(g["usable"], g["problem"], g.get("engine_variant")) for g in nv], [(True, None, "cuda12")] * 2)
|
||||
# a newer card in the PC: Pascal stays an opt-in, the ready-made engine is recommended
|
||||
mixed = [{"index": 0, "name": "NVIDIA GeForce GTX 1080", "vram_gb": 8.0, "arch": "61", "driver": "581.15"},
|
||||
{"index": 1, "name": "NVIDIA GeForce RTX 3060", "vram_gb": 12.0, "arch": "86", "driver": "581.15"}]
|
||||
with mock.patch.object(S, "gpus", lambda: [dict(g) for g in mixed]):
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
hw = s._hardware()
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
by = {g["name"]: g for g in hw["gpus"] if g["vendor"] == "nvidia"}
|
||||
self.assertFalse(by["NVIDIA GeForce GTX 1080"]["usable"])
|
||||
self.assertTrue(by["NVIDIA GeForce RTX 3060"]["usable"])
|
||||
|
||||
def test_an_amd_card_the_installed_engine_has_no_code_for_is_not_usable(self):
|
||||
"""#3203: the engine package lists its GPU code; a card outside it is flagged unusable with the plain reason, so no list offers it."""
|
||||
status = {"usable": True, "engine": {"backend": "hip", "archs": ["gfx1200", "gfx1201"]}}
|
||||
|
||||
Reference in new issue
Block a user