mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 17:38:20 -10:00
Screens and app say 5 experts on several RDNA4 cards that hold half of the experts, as the engine does
The engine gives 5 experts per word on two or more RDNA4 cards that together hold at least half of the experts (4 x R9700), but strata launch --plan, the Speed screen and the app's Speed label called Full quality the default there. dsv41_fit.rdna4_faster() applies the engine's rule with the plan's own numbers: one card the run uses (on Windows one AMD card): yes; several: yes when the plan puts at least half of the set's experts on them (3 or more R9700 for every official set; two hold 37-47%, so they keep 6). strata launch uses it with the chosen model; the model catalog carries rdna4_faster per DeepSeek size for the app (NativeSize, Speed label); --speed help and docs/MODELS.md say it. Other makes and older Radeons are unchanged.
This commit is contained in:
1 parent
386d2e4416
commit
ea2e791d8b
11 files changed
+125
-15
No files matched your search
@@ -117,12 +117,13 @@ export function NativeSettings({ model }: {
|
||||
const rows = (native?.fields ?? []).filter(f => (!f.forModel || (model.toLowerCase().includes(f.forModel) && (f.id !== 'ds_experts' || dsExperts))) && `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const switchRows = switches.filter(sw => `${tr(sw.label)} ${sw.id} ${tr('Speed')}`.toLowerCase().includes(query.toLowerCase()));
|
||||
return <details className="native-settings"><summary>{tr("Native engine settings")}</summary>{tr(tuned?.changed?.length ? <div className="native-tuned"><p className="small"><strong>{tr("Your tuning '{0}' sets", [tuned.preset?.name ?? ''])}</strong></p><ul>{tuned.changed.map(c => { const d = describeChange(c); return <li key={c.flag}><span>{d.name}</span><b>{d.now || tr('off')}</b>{d.was !== null ? <small className="muted">{tr("was {0}", [d.was])}</small> : null}</li>; })}</ul></div> : null)}<p className="small muted">{tr(!original ? 'Reading saved settings… ' : '')}{tr("Settings for this model. Blank values use Strata\u2019s defaults.")}</p><div className="editor-tabs" role="tablist" aria-label={tr("Native settings editor")}>{tr(['fields', 'json', 'tools'].map(t => <button key={t} role="tab" aria-selected={tab === t} onClick={() => chooseTab(t)}>{tr(t === 'fields' ? 'Parameters' : t === 'json' ? 'JSON' : 'MCP tools')}</button>))}</div>
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} model={model} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
<div className="native-settings-actions"><Btn disabled={pending || !original || !dirty} variant="primary" onClick={() => void save()}>{tr(pending ? 'Saving…' : 'Save settings')}</Btn><Btn disabled={pending} onClick={() => { const saved = JSON.parse(original || '{"values":{},"mcp":{}}'); setValues(saved.values); setText(JSON.stringify(saved.values, null, 2)); setMcp(JSON.stringify(saved.mcp, null, 2)); setError(''); }}>{tr("Restore saved")}</Btn></div>{tr(notice ? <p role="status" className="small">{tr(notice)}</p> : null)}{tr(error ? <ErrorNotice message={error} onRetry={() => setRevision(value => value + 1)}/> : null)}
|
||||
</details>;
|
||||
}
|
||||
function NativeFieldInput({ field, value, onChange }: {
|
||||
function NativeFieldInput({ field, value, onChange, model }: {
|
||||
field: NativeField;
|
||||
model?: string;
|
||||
value: any;
|
||||
onChange: (v: any) => void;
|
||||
}) {
|
||||
@@ -131,7 +132,7 @@ function NativeFieldInput({ field, value, onChange }: {
|
||||
if (field.type === 'boolean')
|
||||
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value === null || value === undefined ? '' : String(value)} options={[{ value: '', label: 'Default' }, { value: 'true', label: 'On' }, { value: 'false', label: 'Off' }]} onChange={v => onChange(v === '' ? null : v === 'true')}/>;
|
||||
if (field.id === 'ds_experts')
|
||||
return <DeepSeekSpeed field={field} value={value} onChange={onChange}/>;
|
||||
return <DeepSeekSpeed field={field} model={model} value={value} onChange={onChange}/>;
|
||||
if (field.type === 'enum')
|
||||
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value ?? ''} options={[{ value: '', label: 'Default' }, ...(field.choices ?? []).map(v => ({ value: v, label: v }))]} onChange={v => onChange(v || null)}/>;
|
||||
if (field.type === 'number')
|
||||
@@ -139,9 +140,11 @@ function NativeFieldInput({ field, value, onChange }: {
|
||||
return <input disabled={field.supported === false} className="st-input" aria-label={tr(field.label)} placeholder={tr("Default")} value={Array.isArray(value) ? value.join(', ') : value ?? ''} onChange={e => onChange(e.target.value === '' ? null : field.type === 'array' ? e.target.value.split(',').map(s => s.trim()).filter(Boolean) : field.type === 'number' ? Number(e.target.value) : e.target.value)}/>;
|
||||
}
|
||||
/** DeepSeek's Speed choice. The engine decides the default itself (--experts auto); the labels name the same default (lib/deepseek-speed.ts). */
|
||||
function DeepSeekSpeed({ field, value, onChange }: { field: NativeField; value: any; onChange: (v: any) => void }) {
|
||||
function DeepSeekSpeed({ field, value, onChange, model }: { field: NativeField; model?: string; value: any; onChange: (v: any) => void }) {
|
||||
const native = useNative();
|
||||
const options = speedOptions(native?.hardware?.gpus as RunCard[] | undefined).map(o => ({ ...o, label: tr(o.label) }));
|
||||
// the catalog row of this model says whether the engine gives 5 on RDNA4 cards here (one card, or several holding half of the experts)
|
||||
const size = (native?.catalog?.families ?? []).flatMap(f => f.sizes ?? []).find(s => s.model.toLowerCase() === (model ?? '').toLowerCase());
|
||||
const options = speedOptions(native?.hardware?.gpus as RunCard[] | undefined, size?.rdna4_faster).map(o => ({ ...o, label: tr(o.label) }));
|
||||
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value === null || value === undefined ? '' : String(value)}
|
||||
options={options} onChange={v => onChange(v === '' ? null : v)}/>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { RunCard } from './native-fit';
|
||||
/**
|
||||
* DeepSeek's Speed default, said the way the engine decides it (`--experts auto`, src/dsv41/ds4_experts_rule.hpp): Faster (5 experts a word) when
|
||||
* a card in use is an integrated / unified-memory GPU (Strix Halo, gfx1151: the model shares the PC's memory), when the one card in use is an
|
||||
* RDNA4 Radeon (gfx1200 / gfx1201: the CPU lane does most of the experts), or when the cards in use total 16 GiB or less; otherwise Full quality (6). The app never decides: the engine does; this only names the same default beside the choice.
|
||||
* RDNA4 Radeon (gfx1200 / gfx1201: the CPU lane does most of the experts), when several RDNA4 cards hold half of the model's experts (the catalog's
|
||||
* `rdna4_faster`), or when the cards in use total 16 GiB or less; otherwise Full quality (6). The app never decides: the engine does; this only names the same default beside the choice.
|
||||
* The total is compared in whole MiB (a 16 GB card reports 16376 MiB = 15.99 GiB and counts as 16 GiB or less).
|
||||
*/
|
||||
export const FASTER_MIB = 16 * 1024;
|
||||
@@ -13,18 +14,20 @@ export const isIntegrated = (card: RunCard) => !!(card.uma || card.integrated ||
|
||||
/** One RDNA4 Radeon (gfx1200 / gfx1201: Radeon AI PRO R9700, RX 9070 / 9060). */
|
||||
export const isRdna4 = (card: RunCard) => (card.vendor ?? '').toLowerCase() === 'amd' && /^gfx120[01]/i.test(card.arch ?? '');
|
||||
|
||||
export function fasterByDefault(cards: readonly RunCard[] | null | undefined): boolean {
|
||||
export function fasterByDefault(cards: readonly RunCard[] | null | undefined, rdna4Faster?: boolean | null): boolean {
|
||||
const used = (cards ?? []).filter(card => card.usable !== false);
|
||||
if (!used.length) return false;
|
||||
if (used.some(isIntegrated)) return true;
|
||||
if (used.length === 1 && isRdna4(used[0])) return true;
|
||||
// several RDNA4 cards: the engine gives 5 when together they hold half of the experts of this model (the catalog says, strata_control/dsv41_fit.py rdna4_faster)
|
||||
if (used.length > 1 && used.every(isRdna4) && rdna4Faster === true) return true;
|
||||
const mib = used.reduce((sum, card) => sum + Math.round((card.vram_gb ?? 0) * 1024), 0);
|
||||
return mib > 0 && mib <= FASTER_MIB;
|
||||
}
|
||||
|
||||
/** The three entries of the Speed select: "Faster" or "Full quality" says "(default on this PC)" when it is what the engine picks by itself. */
|
||||
export function speedOptions(cards: readonly RunCard[] | null | undefined): { value: string; label: string }[] {
|
||||
const faster = fasterByDefault(cards);
|
||||
export function speedOptions(cards: readonly RunCard[] | null | undefined, rdna4Faster?: boolean | null): { value: string; label: string }[] {
|
||||
const faster = fasterByDefault(cards, rdna4Faster);
|
||||
const mark = ' (default on this PC)';
|
||||
return [
|
||||
{ value: '', label: faster ? 'Default (Faster on this PC)' : 'Default (Full quality on this PC)' },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { api } from './client';
|
||||
import type { Hardware, RuntimeModels } from './types';
|
||||
export interface GpuChoiceOption {key:string;cards:number[];label:string;verdict:'fits'|'tight'|'spills'|'unknown';words:string;notes:string[];recommended:boolean}
|
||||
export interface GpuChoice {options:GpuChoiceOption[];recommended:string|null;ignored:{name?:string;why_ignored:string}[];cards:number}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;/** DeepSeek: the engine gives 5 experts here on RDNA4 cards (one card, or several holding half of the experts). */rdna4_faster?:boolean|null;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
|
||||
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[]}
|
||||
export interface NativeField {id:string;label:string;group:string;type:'number'|'boolean'|'string'|'enum'|'array';min?:number;max?:number;choices?:string[];hint?:string;supported?:boolean;forModel?:string}
|
||||
export interface NativeJob {id:string;kind:string;guided?:boolean;model?:string;family?:string;state:'running'|'done'|'error'|'cancelled';startedAt:string;summary:string;error:string|null;details?:string;result:Record<string,any>|null;progress?:Record<string,any>|null}
|
||||
|
||||
@@ -36,6 +36,12 @@ test('one RDNA4 Radeon (1 x R9700, 32 GB) defaults to Faster: the CPU lane does
|
||||
assert.equal(fasterByDefault([r9700({ vendor: 'nvidia' })]), false);
|
||||
assert.equal(fasterByDefault([card(32)]), false, 'no arch in the report: the old rule');
|
||||
assert.equal(fasterByDefault([r9700(), r9700({ index: 1 })]), false, 'two cards are the other engine rule (they must hold half of the experts)');
|
||||
assert.equal(fasterByDefault([r9700(), r9700({ index: 1 })], false), false, 'the catalog says the two cards hold under half');
|
||||
const four = [0, 1, 2, 3].map(index => r9700({ index }));
|
||||
assert.equal(fasterByDefault(four, true), true, 'four R9700 hold most of the experts: the engine runs 5, so the label says Faster');
|
||||
assert.equal(fasterByDefault(four, null), false, 'no answer from the catalog: the old rule');
|
||||
assert.equal(fasterByDefault([r9700({ vendor: 'nvidia' }), r9700({ index: 1 })], true), false, 'only for RDNA4 cards');
|
||||
assert.deepEqual(speedOptions(four, true).map(o => o.label), ['Default (Faster on this PC)', 'Faster (default on this PC)', 'Full quality']);
|
||||
assert.equal(fasterByDefault([r9700(), r9700({ index: 1, usable: false })]), true, 'a card the engine cannot use does not count');
|
||||
assert.deepEqual(speedOptions([r9700()]).map(o => o.label), ['Default (Faster on this PC)', 'Faster (default on this PC)', 'Full quality']);
|
||||
});
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ strata launch --family deepseek41 --model ist-das-2.00bpw
|
||||
|
||||
**Speed: Faster or Full quality.** The app asks, and the Terminal takes `--speed faster|full` (or `--full-quality`). Faster uses 5 of the
|
||||
model's 6 experts per word, a small quality change that is usually not noticeable. It is the default on a PC with 16 GB of graphics
|
||||
memory or less, on a Ryzen AI Max, and on a single Radeon RX 9000 / AI PRO R9700 card (and on several of them when they hold half of the experts). Full quality uses all 6. It needs DeepSeek engine 0.1.50 or newer, which the app and `strata launch` fetch.
|
||||
memory or less, on a Ryzen AI Max, and on a single Radeon RX 9000 / AI PRO R9700 card, and on several of them when together they hold half of the experts (three or more R9700 cards; two hold about 40% of the 2.25 set, so they keep 6). Full quality uses all 6. It needs DeepSeek engine 0.1.50 or newer, which the app and `strata launch` fetch.
|
||||
|
||||
**Measured on:** IST-DAS 2.00 bpw, Strata 0.2.4 (before the Speed choice).
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ def add_parsers(sp: argparse._SubParsersAction) -> list[argparse.ArgumentParser]
|
||||
"(not enough graphics memory, or a model it is not for) the MTP is used and the start says why")
|
||||
g.add_argument("--speed", type=_choice("a speed", S.SPEED_MODES), metavar="faster|full",
|
||||
help="DeepSeek: faster uses 5 of 6 experts per word (a small quality change, usually not noticeable; the default on a PC with 16 GB "
|
||||
"of graphics memory or less, and on a single Radeon RX 9000 / AI PRO R9700 card), full uses all 6")
|
||||
"of graphics memory or less, and on a single Radeon RX 9000 / AI PRO R9700 card, or several that hold half of the experts), full uses all 6")
|
||||
g.add_argument("--full-quality", action="store_true", help="DeepSeek: all 6 experts per word (the same as --speed full), for scripts")
|
||||
g.add_argument("--gpu", metavar="LIST", help="graphics card numbers to use, e.g. 0 or 0,1 (default: the engine's choice)")
|
||||
g.add_argument("--gpu-roles", metavar="LIST", help="a role for each graphics card: main, helper or off, e.g. 1=main,0=helper (cards you do not name are off). "
|
||||
|
||||
@@ -476,7 +476,16 @@ def uses_integrated_gpu(computer, s: "S.LaunchSettings") -> bool:
|
||||
|
||||
def faster_by_default(computer, s: "S.LaunchSettings") -> bool:
|
||||
"""The engine's `--experts auto` rule: Faster (5 experts) on an integrated GPU, on a single RDNA4 card, or when the cards used total 16 GiB or less."""
|
||||
return uses_integrated_gpu(computer, s) or S.single_rdna4(used_cards(computer, s)) or S.faster_default(used_vram_gb(computer, s))
|
||||
cards = used_cards(computer, s)
|
||||
if uses_integrated_gpu(computer, s) or S.single_rdna4(cards) or S.faster_default(used_vram_gb(computer, s)):
|
||||
return True
|
||||
if len(cards) < 2 or not s.model:
|
||||
return False
|
||||
from strata_control import dsv41_fit # several RDNA4 cards: the engine's other rule (they hold half of the experts), the plan's numbers
|
||||
try:
|
||||
return dsv41_fit.rdna4_faster(cards, s.model, getattr(computer, "engine_vendor", None), named=bool(s.gpus or s.gpu_roles))
|
||||
except Exception: # noqa: BLE001 - the screens' words must not break a launch; the engine says what it did
|
||||
return False
|
||||
|
||||
|
||||
EXPERTS_LINE = re.compile(r"experts per word: (\d+)")
|
||||
|
||||
@@ -179,6 +179,26 @@ def run_cards(cards: list[dict] | None, vendor: str | None = None, *, windows: b
|
||||
|
||||
|
||||
|
||||
def _is_rdna4(card: dict) -> bool:
|
||||
return str(card.get("vendor") or "").lower() == "amd" and str(card.get("arch") or "").lower().startswith(DSV41_HIP_ARCHS)
|
||||
|
||||
|
||||
def rdna4_faster(cards: list[dict] | None, model: str | None, vendor: str | None = None, named: bool = False) -> bool:
|
||||
"""Does the engine's `--experts auto` give 5 on these RDNA4 cards (src/dsv41/ds4_experts_rule.hpp)? One card the run uses: yes (the CPU lane does most
|
||||
of the experts: 1 x R9700 +16% decode, -24% agent loop, KL +0.004). Two or more: yes when together they hold at least half of the experts, which is what
|
||||
this plan says for the set (4 x R9700: 12,260 of 15,360; 2 x R9700: about 40% of the 2.25 set, so 6). The same cards `run_cards` picks (on Windows one
|
||||
AMD card). False for anything else, so the screens never say Full quality where the engine runs 5, nor the reverse."""
|
||||
keep, _ = run_cards(cards, vendor, named=named)
|
||||
if not keep or not all(_is_rdna4(c) for c in keep):
|
||||
return False
|
||||
if len(keep) == 1:
|
||||
return True
|
||||
if not variant(model):
|
||||
return False
|
||||
p = plan(keep, None, None, model=model, vendor=vendor, named=True)
|
||||
return p["experts_in_vram"] >= 0.5 * p["experts_total"]
|
||||
|
||||
|
||||
def uma_card(cards: list[dict] | None) -> dict | None:
|
||||
"""The integrated Radeon a run uses, when the graphics memory is the PC's RAM (Ryzen AI Max, gfx1151): setup marks it `uma` (and
|
||||
`integrated`); a discrete card never is. None on every other computer."""
|
||||
|
||||
@@ -120,3 +120,42 @@ class MeasuredCardAccounting(unittest.TestCase):
|
||||
a = self.plan(1, 32.0, "amd", model="IST-DAS-2.25bpw", ram=126.0)["vram_experts_gb"]
|
||||
b = self.plan(1, 32.0, "amd", model="UD-IQ1_M", ram=126.0)["vram_experts_gb"] # 7.6 GB of dense weights against 5.9
|
||||
self.assertLess(b, a)
|
||||
|
||||
|
||||
class Rdna4Faster(unittest.TestCase):
|
||||
"""rdna4_faster: the engine's `--experts auto` on RDNA4 cards, said by the screens and the app (ds4_experts_rule.hpp single_rdna4 / multi_rdna4)."""
|
||||
|
||||
@staticmethod
|
||||
def cards(n, vendor="amd", arch="gfx1201", gb=32.0):
|
||||
return [{"index": i, "name": "card", "vram_gb": gb, "vendor": vendor, "arch": arch} for i in range(n)]
|
||||
|
||||
def test_one_card_yes_two_no_three_and_four_yes_on_linux(self):
|
||||
orig = F._on_windows
|
||||
F._on_windows = lambda: False
|
||||
try:
|
||||
for model in ("IST-DAS-2.00bpw", "IST-DAS-2.25bpw", "UD-IQ1_S", "UD-IQ1_M"):
|
||||
self.assertTrue(F.rdna4_faster(self.cards(1), model, "amd"), model)
|
||||
self.assertFalse(F.rdna4_faster(self.cards(2), model, "amd"), model) # 37-47% of the experts on two cards
|
||||
self.assertTrue(F.rdna4_faster(self.cards(3), model, "amd"), model)
|
||||
self.assertTrue(F.rdna4_faster(self.cards(4), model, "amd"), model)
|
||||
finally:
|
||||
F._on_windows = orig
|
||||
|
||||
def test_not_for_other_cards_or_an_unknown_model(self):
|
||||
orig = F._on_windows
|
||||
F._on_windows = lambda: False
|
||||
try:
|
||||
self.assertFalse(F.rdna4_faster(self.cards(4, vendor="nvidia"), "IST-DAS-2.25bpw", "nvidia"))
|
||||
self.assertFalse(F.rdna4_faster(self.cards(4, arch="gfx1100"), "IST-DAS-2.25bpw", "amd"))
|
||||
self.assertFalse(F.rdna4_faster(self.cards(4), None, "amd"))
|
||||
self.assertFalse(F.rdna4_faster([], "IST-DAS-2.25bpw", "amd"))
|
||||
finally:
|
||||
F._on_windows = orig
|
||||
|
||||
def test_windows_runs_one_amd_card(self):
|
||||
orig = F._on_windows
|
||||
F._on_windows = lambda: True
|
||||
try:
|
||||
self.assertTrue(F.rdna4_faster(self.cards(2), "IST-DAS-2.25bpw", "amd")) # one card of the two: the single-card rule
|
||||
finally:
|
||||
F._on_windows = orig
|
||||
@@ -136,10 +136,29 @@ class Resolve(unittest.TestCase):
|
||||
for other in ({**r9700, "arch": "gfx1100"}, {**r9700, "vendor": "nvidia"}, {k: v for k, v in r9700.items() if k != "arch"}):
|
||||
self.assertFalse(S.single_rdna4([other]), other)
|
||||
self.assertFalse(faster_by_default(mock.Mock(usable_gpus=[other]), s), other)
|
||||
two = mock.Mock(usable_gpus=[r9700, {**r9700, "index": 1}])
|
||||
self.assertFalse(faster_by_default(two, s))
|
||||
two = mock.Mock(usable_gpus=[r9700, {**r9700, "index": 1}], engine_vendor="amd")
|
||||
self.assertFalse(faster_by_default(two, s)) # no model chosen yet: the old rule
|
||||
with mock.patch("strata_control.dsv41_fit._on_windows", lambda: False):
|
||||
self.assertFalse(faster_by_default(two, S.LaunchSettings(model="IST-DAS-2.25bpw")), "2 x R9700 hold about 40% of the 2.25 set: the engine says 6")
|
||||
with mock.patch("strata_control.dsv41_fit._on_windows", lambda: True): # Windows runs one AMD card: a single RDNA4 card, the engine says 5
|
||||
self.assertTrue(faster_by_default(two, S.LaunchSettings(model="IST-DAS-2.25bpw")))
|
||||
self.assertTrue(faster_by_default(two, S.LaunchSettings(gpus=[1]))) # one of the two named: a single card
|
||||
|
||||
def test_several_rdna4_cards_holding_half_of_the_experts_are_faster_by_default(self): # 4 x R9700: the engine says 5, so the screens must not say Full quality
|
||||
r9700 = {"index": 0, "vram_gb": 32, "vendor": "amd", "arch": "gfx1201", "name": "Radeon AI PRO R9700"}
|
||||
cards = lambda n: mock.Mock(usable_gpus=[{**r9700, "index": i} for i in range(n)], engine_vendor="amd")
|
||||
s = lambda **kw: S.LaunchSettings(family="deepseek41", model=kw.pop("model", "IST-DAS-2.25bpw"), **kw)
|
||||
with mock.patch("strata_control.dsv41_fit._on_windows", lambda: False):
|
||||
self.assertTrue(faster_by_default(cards(4), s()))
|
||||
self.assertTrue(faster_by_default(cards(3), s()))
|
||||
self.assertFalse(faster_by_default(cards(2), s()))
|
||||
self.assertFalse(faster_by_default(cards(2), s(model="UD-IQ1_S")), "2 x R9700 hold 47% of the smallest-but-one set: still under half")
|
||||
self.assertTrue(faster_by_default(cards(3), s(model="UD-IQ1_M")))
|
||||
self.assertTrue(faster_by_default(cards(4), s(gpus=[0])), "one card named: a single RDNA4 card")
|
||||
self.assertFalse(faster_by_default(cards(2), s(model=None)))
|
||||
nv = mock.Mock(usable_gpus=[{**r9700, "vendor": "nvidia", "index": i} for i in range(4)], engine_vendor="nvidia")
|
||||
self.assertFalse(faster_by_default(nv, s()))
|
||||
|
||||
def test_only_the_cards_the_launch_uses_are_summed(self):
|
||||
comp = mock.Mock(usable_gpus=[{"index": 0, "vram_gb": 12}, {"index": 1, "vram_gb": 24}])
|
||||
self.assertEqual(used_vram_gb(comp, S.LaunchSettings()), 36)
|
||||
|
||||
@@ -1332,6 +1332,9 @@ class Strata:
|
||||
"engine_min": ".".join(map(str, d["engine"])) if d.get("engine") else None,
|
||||
"memory_gb": round(float(d["ram_gb"]) if d.get("mem_total") else float(d.get("arena_gb") or d["ram_gb"]) + 1.0, 1),
|
||||
"on_this_pc": verdict, "installed": tag in have, "id": tag,
|
||||
# DeepSeek only: does the engine's `--experts auto` give 5 here? (strata_control/dsv41_fit.py rdna4_faster: one RDNA4 card, or
|
||||
# several holding half of the experts); the app's Speed label says so. None: the app's own rules (16 GiB, integrated) decide.
|
||||
"rdna4_faster": self.dsv41_rdna4_faster(hw, m) if own and own.get("multi_file") else None,
|
||||
# which graphics cards to use for it (strata_control/gpu_fit.py, the one rule app / Terminal / setup share):
|
||||
# only with two or more cards that count; "recommended" names the pre-selected option
|
||||
"gpu_options": (self.dsv41_options(hw, m) if own and own.get("multi_file")
|
||||
@@ -1456,6 +1459,14 @@ class Strata:
|
||||
f"({pl['ram_needed_gb']:.0f} GB of RAM)")
|
||||
return "not checked (this computer's memory could not be read)"
|
||||
|
||||
@staticmethod
|
||||
def dsv41_rdna4_faster(hw, model) -> bool | None:
|
||||
from strata_control import dsv41_fit
|
||||
try:
|
||||
return dsv41_fit.rdna4_faster(list((hw or {}).get("gpus", [])), model, (hw or {}).get("engine_vendor"))
|
||||
except Exception: # noqa: BLE001 - a convenience for the label: never break the model list
|
||||
return None
|
||||
|
||||
def dsv41_options(self, hw, model=None) -> dict | None:
|
||||
"""gpu_fit-shaped card choices for DeepSeek-V4.1-Flash: every card (recommended: the model is bigger than any one card), or the
|
||||
largest alone; each with the placement words."""
|
||||
|
||||
Reference in new issue
Block a user