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:
Niko1221 committed 2026-10-22 13:47:41 +02:00
1 parent d9042a16bd
commit 0351ecb11d
9 files changed
+104 -7

No files matched your search

+5 -2
View File
@@ -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 };
+19
View File
@@ -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
View File
@@ -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):
+1 -1
View File
@@ -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()}")
+5 -2
View File
@@ -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"
+4
View File
@@ -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"}
],
+22
View File
@@ -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")
+10
View File
@@ -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"):
+36
View File
@@ -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"]}}