mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 18:35:04 -10:00
tuning feedback: structured outcome (faster / already_fastest / busy_pc / failed / stopped + values), tuned values readable (presets effective, settings carry them), which tuning is active and off/on, applied at every start (restart and unload/load tests)
This commit is contained in:
1 parent
9f24934663
commit
dab63e9c38
21 files changed
+407
-29
No files matched your search
@@ -43,6 +43,11 @@ KEYS = [
|
||||
"Start tuning",
|
||||
"Tuning your model",
|
||||
'Answers can differ from run to run on this PC even with the standard setup; this setting does not add to that.',
|
||||
"Using your tuning '{0}'",
|
||||
'Switch off',
|
||||
'Switch on',
|
||||
'Your tuning is off. The standard settings are used.',
|
||||
'Your PC was busy, so the measurements were not reliable. Nothing was changed. Try again when it is quiet.',
|
||||
]
|
||||
|
||||
T = {
|
||||
@@ -62,6 +67,11 @@ T = {
|
||||
"长提示的编程", "允许可能略微改变回答的设置", "已保存的调优", "使用中", "使用",
|
||||
"再次检查(约 2 分钟)", "再次调优", "开始调优", "正在调优你的模型",
|
||||
'在这台电脑上,即使使用标准设置,每次运行的回答也可能不同;此设置不会增加这种差异。',
|
||||
'正在使用你的调优“{0}”',
|
||||
'关闭',
|
||||
'开启',
|
||||
'你的调优已关闭,使用标准设置。',
|
||||
'你的电脑当时很忙,测量结果不可靠。没有做任何改动。请在电脑空闲时再试。',
|
||||
],
|
||||
"ja": [
|
||||
"このモデルをチューニング", "あなたの PC で最も速い設定を探しています…", "残り約 {0} 分",
|
||||
@@ -79,6 +89,11 @@ T = {
|
||||
"長いプロンプトでのコーディング", "回答がわずかに変わる可能性のある設定も許可する", "保存済みチューニング", "使用中", "使用する",
|
||||
"もう一度チェック(約 2 分)", "もう一度チューニング", "チューニングを開始", "モデルをチューニング中",
|
||||
'このPCでは標準設定でも実行のたびに回答が変わることがあります。この設定がそれを増やすことはありません。',
|
||||
'あなたのチューニング「{0}」を使用中',
|
||||
'オフにする',
|
||||
'オンにする',
|
||||
'チューニングはオフです。標準設定を使用します。',
|
||||
'PCが忙しく、測定結果が信頼できませんでした。何も変更していません。PCが空いているときにもう一度お試しください。',
|
||||
],
|
||||
"de": [
|
||||
"Dieses Modell abstimmen", "Die schnellsten Einstellungen für deinen PC werden gesucht…", "noch etwa {0} Min.",
|
||||
@@ -96,6 +111,11 @@ T = {
|
||||
"Programmieren mit langen Prompts", "Einstellungen erlauben, die Antworten leicht verändern können", "Gespeicherte Abstimmungen", "In Gebrauch", "Verwenden",
|
||||
"Erneut prüfen (etwa 2 Minuten)", "Erneut abstimmen", "Abstimmung starten", "Dein Modell wird abgestimmt",
|
||||
'Auf diesem PC können die Antworten von Lauf zu Lauf abweichen, auch mit der Standardeinstellung; diese Einstellung verstärkt das nicht.',
|
||||
'Deine Abstimmung „{0}“ wird verwendet',
|
||||
'Ausschalten',
|
||||
'Einschalten',
|
||||
'Deine Abstimmung ist aus. Es gelten die Standardeinstellungen.',
|
||||
'Dein PC war ausgelastet, daher waren die Messungen nicht zuverlässig. Es wurde nichts geändert. Versuche es erneut, wenn er ruhig ist.',
|
||||
],
|
||||
"es": [
|
||||
"Ajustar este modelo", "Buscando los ajustes más rápidos para tu PC…", "faltan unos {0} min",
|
||||
@@ -113,6 +133,11 @@ T = {
|
||||
"Programar con prompts largos", "Permitir ajustes que pueden cambiar ligeramente las respuestas", "Ajustes guardados", "En uso", "Usar",
|
||||
"Comprobar de nuevo (unos 2 minutos)", "Ajustar de nuevo", "Empezar a ajustar", "Ajustando tu modelo",
|
||||
'En este PC las respuestas pueden variar de una ejecución a otra incluso con el ajuste estándar; este ajuste no lo aumenta.',
|
||||
'Usando tu ajuste «{0}»',
|
||||
'Desactivar',
|
||||
'Activar',
|
||||
'Tu ajuste está desactivado. Se usan los ajustes estándar.',
|
||||
'Tu PC estaba ocupado, así que las mediciones no fueron fiables. No se cambió nada. Inténtalo de nuevo cuando esté tranquilo.',
|
||||
],
|
||||
"ru": [
|
||||
"Настроить эту модель", "Ищем самые быстрые настройки для вашего ПК…", "осталось примерно {0} мин.",
|
||||
@@ -130,6 +155,11 @@ T = {
|
||||
"Программирование с длинными промптами", "Разрешить настройки, которые могут немного изменить ответы", "Сохранённые настройки", "Используется", "Использовать",
|
||||
"Проверить снова (около 2 минут)", "Настроить снова", "Начать настройку", "Настройка вашей модели",
|
||||
'На этом ПК ответы могут отличаться от запуска к запуску даже при стандартных настройках; эта настройка этого не усиливает.',
|
||||
'Используется ваша настройка «{0}»',
|
||||
'Выключить',
|
||||
'Включить',
|
||||
'Ваша настройка выключена. Используются стандартные параметры.',
|
||||
'Ваш ПК был занят, поэтому измерения были ненадёжными. Ничего не изменено. Повторите, когда он освободится.',
|
||||
],
|
||||
"pt": [
|
||||
"Ajustar este modelo", "Procurando as configurações mais rápidas para o seu PC…", "faltam cerca de {0} min",
|
||||
@@ -147,6 +177,11 @@ T = {
|
||||
"Programação com prompts longos", "Permitir configurações que podem mudar um pouco as respostas", "Ajustes salvos", "Em uso", "Usar",
|
||||
"Verificar de novo (cerca de 2 minutos)", "Ajustar de novo", "Iniciar ajuste", "Ajustando o seu modelo",
|
||||
'Neste PC as respostas podem variar de uma execução para outra mesmo com a configuração padrão; esta configuração não aumenta isso.',
|
||||
'Usando o seu ajuste “{0}”',
|
||||
'Desligar',
|
||||
'Ligar',
|
||||
'O seu ajuste está desligado. As configurações padrão são usadas.',
|
||||
'O seu PC estava ocupado, então as medições não foram confiáveis. Nada foi alterado. Tente de novo quando ele estiver tranquilo.',
|
||||
],
|
||||
"ko": [
|
||||
"이 모델 최적화", "내 PC에서 가장 빠른 설정을 찾는 중…", "약 {0}분 남음",
|
||||
@@ -164,6 +199,11 @@ T = {
|
||||
"긴 프롬프트로 코딩", "답변을 조금 바꿀 수 있는 설정 허용", "저장된 최적화", "사용 중", "사용",
|
||||
"다시 점검 (약 2분)", "다시 최적화", "최적화 시작", "모델을 최적화하는 중",
|
||||
'이 PC에서는 표준 설정에서도 실행할 때마다 답변이 달라질 수 있으며, 이 설정이 그 차이를 키우지는 않습니다.',
|
||||
'내 최적화 ‘{0}’ 사용 중',
|
||||
'끄기',
|
||||
'켜기',
|
||||
'최적화가 꺼져 있습니다. 표준 설정을 사용합니다.',
|
||||
'PC가 바빠서 측정 결과를 믿을 수 없었습니다. 아무것도 변경하지 않았습니다. PC가 한가할 때 다시 시도하세요.',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -775,6 +775,8 @@ async function api(req, res, path, method, url) {
|
||||
if(path==='/api/native/benchmark'&&method==='GET')return json(res,200,native.benchmarkReport());
|
||||
/* The auto-tuner's saved tunings (Strata Control's presets, shared with the Terminal). Starting a tune or a re-check is POST /api/native/operations with kind autotune / recheck. */
|
||||
if(path==='/api/native/tune/status'&&method==='GET')return json(res,200,await native.tuneStatus({model:url.searchParams.get('model'),resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined}));
|
||||
if(path==='/api/native/tune/effective'&&method==='GET')return json(res,200,await native.tuneEffective({model:url.searchParams.get('model'),resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined}));
|
||||
if(path==='/api/native/tune/off'&&method==='POST'){const body=await readBody(req);return json(res,200,await native.tuneOff({model:body.model,resourceProfile:body.resourceProfile,vision:body.vision}));}
|
||||
if(path==='/api/native/tune/presets'&&method==='GET')return json(res,200,await native.tunePresets.list());
|
||||
if(path==='/api/native/tune/undo'&&method==='POST'){const body=await readBody(req);return json(res,200,await native.tunePresets.undoFor({model:body.model,resourceProfile:body.resourceProfile,vision:body.vision}));}
|
||||
const tunePreset=/^\/api\/native\/tune\/presets\/([0-9a-f]{8,32})\/(apply|rename|delete|undo)$/.exec(path);
|
||||
@@ -784,7 +786,8 @@ async function api(req, res, path, method, url) {
|
||||
const nativeInstance=/^\/api\/native\/instances\/([a-z0-9-]+)\/(unload|default)$/.exec(path);
|
||||
if(nativeInstance&&method==='POST')return json(res,200,nativeInstance[2]==='unload'?await native.unload(nativeInstance[1]):native.setDefault(nativeInstance[1]));
|
||||
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings$/.exec(path);
|
||||
if(nativeSettings&&method==='GET')return json(res,200,native.settings(nativeSettings[1]));
|
||||
/* Expert mode shows what will actually run: the saved tuning's values come with the settings (null when none applies or unreadable). */
|
||||
if(nativeSettings&&method==='GET'){const settings=native.settings(nativeSettings[1]);let tuned=null;try{const t=await native.tuneEffective({model:nativeSettings[1],resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined});tuned=t.applied?t:null;}catch{tuned=null;}return json(res,200,{...settings,tuned});}
|
||||
if(nativeSettings&&method==='PATCH')return json(res,200,native.save(nativeSettings[1],await readBody(req)));
|
||||
|
||||
// Credentials are handled only on the host. Responses and events expose a key-set flag.
|
||||
|
||||
@@ -14,6 +14,7 @@ const USES = new Set(['chat', 'coding']);
|
||||
export function resultSentence(result) {
|
||||
if (!result) return 'The tune did not finish.';
|
||||
const name = result.preset?.name;
|
||||
if (result.code === 'busy_pc') return 'Your PC was busy, so the measurements were not reliable. Nothing was changed. Try again when it is quiet.';
|
||||
if (result.outcome === 'improved') {
|
||||
const gain = Math.max(0, Math.round(result.gain_pct ?? 0));
|
||||
return `+${gain}% faster answers${name ? `, saved as '${name}'` : ''}`;
|
||||
@@ -54,6 +55,10 @@ export function createAutoTune({ launcher, workDir, spawnProcess = spawn, pollMs
|
||||
run,
|
||||
/** Which saved tuning these choices start with, and whether it is still valid (reads only). */
|
||||
async status(configPath) { return need(await run(['presets', 'status', configPath, '--output', 'json'], { timeoutMs: 45_000 }), 'The saved tuning could not be read.'); },
|
||||
/** What a start will really run: the saved tuning's values on top of the configuration (Expert mode shows these). */
|
||||
async effective(configPath) { return need(await run(['presets', 'effective', configPath, '--output', 'json'], { timeoutMs: 45_000 }), 'The tuned values could not be read.'); },
|
||||
/** Switch the saved tuning off for these choices (kept, can be switched on again with apply). */
|
||||
async off(configPath) { return need(await run(['presets', 'off', configPath, '--output', 'json'], { timeoutMs: 45_000 }), 'The saved tuning could not be switched off.'); },
|
||||
async list() { return need(await run(['presets', 'list', '--output', 'json'], { timeoutMs: 30_000 }), 'The saved tunings could not be read.'); },
|
||||
async action(kind, id, extra = {}) {
|
||||
if (!/^[0-9a-f]{8,32}$/.test(String(id ?? '')) && kind !== 'undo') throw new Error('Choose a saved tuning.');
|
||||
|
||||
@@ -184,6 +184,8 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
/* What the tuner measures is what a load would start: the saved model config with the user's choices and the resource profile. */
|
||||
function tuneConfig(args){const {config,entry}=record(args.model);if(!entry.ready)throw new Error('Prepare the model before tuning it.');const {cfg}=buildConfig({model:args.model,vision:args.vision,resourceProfile:args.resourceProfile,gpus:args.gpus},config);return {path:writeTuneConfig(directory,args.model,cfg),entry};}
|
||||
async function tuneStatus(args){await refresh();const {path}=tuneConfig(args);return autotuner.status(path);}
|
||||
async function tuneEffective(args){await refresh();const {path}=tuneConfig(args);return autotuner.effective(path);}
|
||||
async function tuneOff(args){await refresh();const {path}=tuneConfig(args);return autotuner.off(path);}
|
||||
async function autotune(job,args){
|
||||
await refresh(true);if(job.state!=='running')return;
|
||||
if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(engine.notice??'No engine for this PC was found.');}
|
||||
@@ -201,5 +203,5 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
async function close(){closing=true;clearInterval(timer);const operations=[];for(const job of jobs.values()){job.abort?.abort();if(job.process)operations.push(stopProcess(job.process));if(job.state==='running')job.state='cancelled';}await Promise.all(operations);await Promise.all([...instances.keys()].map(id=>unload(id,true).catch(()=>{})));disconnect();}
|
||||
// Remove nonserializable process references from public jobs.
|
||||
const publicState=()=>{const value=state();value.jobs=value.jobs.map(({process,...job})=>job);return value;};
|
||||
return {state:publicState,tuneStatus,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,configure,setDownloadDirectory,plan,settings,save,operation,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),close};
|
||||
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,configure,setDownloadDirectory,plan,settings,save,operation,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),close};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { useStore } from '../lib/store';
|
||||
import { NativePresets } from './NativePresets';
|
||||
import { NativeBenchmark } from './NativeOperations';
|
||||
import { NativeTune } from './NativeTune';
|
||||
import { NativeTuneBadge } from './NativeTuneBadge';
|
||||
import { nativeCatalogModels, catalogVersionLabel } from '../lib/native-catalog';
|
||||
export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine }: {
|
||||
onOtherModels: () => void;
|
||||
@@ -92,6 +93,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
{tr(active ? <NativeProgress job={active}/> : null)}
|
||||
{tr(job?.state === 'error' ? <ErrorNotice message={job.error ?? job.summary} action={tr("Retry")} onRetry={() => void load()}/> : null)}
|
||||
{tr(error ? <ErrorNotice message={error}/> : null)}
|
||||
{selectedSize && installed?.ready ? <NativeTuneBadge model={selectedSize.id} resourceProfile={resourceProfile} vision={selectedSize.images ? vision : 'no'}/> : null}
|
||||
<div className="native-model-actions">{tr(onReady && native?.instances.some(i => i.status === 'ready') ? <Btn variant="primary" onClick={onReady}>{tr("Start chatting")}</Btn> : <><Btn icon="gauge" disabled={!installed?.ready || !native?.available || !!active || !selectedSize} onClick={() => setTuneOpen(true)}>{tr("Tune model")}</Btn><Btn variant="primary" icon="models" disabled={pending || !native?.available || !!active || !selectedSize || expert && deviceChoice === 'split' && gpus.length < 2} onClick={() => void load()}>{tr(pending ? 'Checking…' : installed?.ready ? 'Load model' : 'Download & load')}</Btn></>)}</div>
|
||||
</div>
|
||||
<aside className="stack stack--lg"><section className="st-card"><h2 className="h4">{tr("This computer")}</h2><p>{tr(native?.hardware?.ram_gb || hardware?.memory.totalGb || '—')}{tr(" GB RAM")}</p>{tr(native?.hardware?.gpus.length ? native.hardware.gpus.map(g => <p className="small" key={g.index}>{tr(g.name)} · {tr(g.vram_gb)}{tr(" GB")}</p>) : <p className="small muted">{tr("No supported GPU reported by this installation.")}</p>)}<p className="small muted">{tr("Compatibility and sizes come from your installed Strata version.")}</p></section><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function NativeTune({ open, onClose, model, resourceProfile, vision }: {
|
||||
actions = <><Btn onClick={() => void act(() => nativeAction(`operations/${job!.id}/cancel`))} disabled={busy}>{tr("Stop")}</Btn></>;
|
||||
} else if (phase === 'result') {
|
||||
const improved = result?.outcome === 'improved' || result?.verdict === 'keep';
|
||||
const text = stopped ? tr("Stopped. Your settings were not changed.") : job?.state === 'error' ? tr("The tune did not finish. Your settings were not changed.") : result?.verdict ? (result.verdict === 'keep' ? tr("Still as fast as when it was saved.") : result.verdict === 'retune' ? tr("It is slower than before. You can tune again.") : tr("Could not tell right now. Your tuning was kept.")) : result?.outcome === 'improved' ? tr("+{0}% faster answers, saved as '{1}'", [Math.max(0, Math.round(result.gain_pct ?? 0)), result.preset?.name ?? '']) : result?.outcome === 'default_confirmed' ? tr("Your current settings are already the fastest we found.") : (result?.message ?? tr("Nothing was changed."));
|
||||
const text = stopped ? tr("Stopped. Your settings were not changed.") : job?.state === 'error' ? tr("The tune did not finish. Your settings were not changed.") : result?.verdict ? (result.verdict === 'keep' ? tr("Still as fast as when it was saved.") : result.verdict === 'retune' ? tr("It is slower than before. You can tune again.") : tr("Could not tell right now. Your tuning was kept.")) : result?.outcome === 'improved' ? tr("+{0}% faster answers, saved as '{1}'", [Math.max(0, Math.round(result.gain_pct ?? 0)), result.preset?.name ?? '']) : result?.code === 'busy_pc' ? tr("Your PC was busy, so the measurements were not reliable. Nothing was changed. Try again when it is quiet.") : result?.outcome === 'default_confirmed' ? tr("Your current settings are already the fastest we found.") : (result?.message ?? tr("Nothing was changed."));
|
||||
body = <div className="tune-progress">{orbit(improved || result?.outcome === 'default_confirmed')}<h3 role="status">{text}</h3>
|
||||
{result?.outcome === 'default_confirmed' && result.preset ? <p className="small muted">{tr("Saved as '{0}', so it is not tuned again when you reopen the app.", [result.preset.name])}</p> : null}
|
||||
{result?.outcome === 'improved' ? <><p className="small muted">{tr("Speed: {0} → {1} tok/s", [Math.round((result.base?.decode_tok_s ?? 0) * 10) / 10, Math.round((result.chosen?.decode_tok_s ?? 0) * 10) / 10])}</p>{notice(result.answers?.class, result.answers?.default_repeatable)}</> : null}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Btn } from './ui';
|
||||
import { answerNotice, tunePresetAction, tuneOff, tuneStatus, type TuneStatus } from '../lib/native-tune';
|
||||
import { useNative } from '../lib/native';
|
||||
|
||||
/**
|
||||
* Which saved tuning a start will use, in plain words, and a way to switch it off (back to the standard settings) or on again.
|
||||
* Shown on the Models screen so nobody has to guess what runs after an unload or a restart. The tuning is applied by Strata at every
|
||||
* start (see strata_control/presets.py); this only reads it and flips it. Layout is the UI worker's: it is one line of text and one button.
|
||||
*/
|
||||
export function NativeTuneBadge({ model, resourceProfile, vision }: { model: string; resourceProfile: 'work' | 'performance'; vision: string }) {
|
||||
const native = useNative();
|
||||
const [status, setStatus] = useState<TuneStatus | null>(null), [busy, setBusy] = useState(false), [error, setError] = useState('');
|
||||
const jobs = native?.jobs.filter(j => j.state === 'running').length ?? 0;
|
||||
const read = useCallback(async () => {
|
||||
try { setStatus(await tuneStatus(model, resourceProfile, vision)); setError(''); }
|
||||
catch { setStatus(null); }
|
||||
}, [model, resourceProfile, vision]);
|
||||
useEffect(() => { void read(); }, [read, jobs, native?.instances.length]);
|
||||
async function flip(fn: () => Promise<unknown>) {
|
||||
setBusy(true); setError('');
|
||||
try { await fn(); await read(); }
|
||||
catch (e) { setError(e instanceof Error ? e.message : String(e)); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
if (!status || (!status.active && !status.last_active)) return null;
|
||||
const active = status.active;
|
||||
return <p className="small muted" data-tuning-active={!!active} role="status">
|
||||
{active ? <>{tr("Using your tuning '{0}'", [active.name])}{status.state === 'hard' ? <> {'·'} {tr("This PC changed since it was tuned, so these settings are not used until you tune again.")}</> : null}
|
||||
{active.answers && active.answers.class !== 'exact' ? <> {'·'} {tr(answerNotice(active.answers.class, active.answers.default_repeatable))}</> : null}{' '}
|
||||
<Btn size="sm" variant="ghost" disabled={busy} onClick={() => void flip(() => tuneOff(model, resourceProfile, vision))}>{tr("Switch off")}</Btn></>
|
||||
: <>{tr("Your tuning is off. The standard settings are used.")}{' '}
|
||||
<Btn size="sm" variant="ghost" disabled={busy} onClick={() => void flip(() => tunePresetAction(status.last_active!.id, 'apply'))}>{tr("Switch on")}</Btn></>}
|
||||
{error ? <span className="inline-error" role="alert"> {error}</span> : null}
|
||||
</p>;
|
||||
}
|
||||
@@ -25,6 +25,9 @@ export interface TuneStatus {
|
||||
reasons: string[];
|
||||
presets: TunePreset[];
|
||||
can_undo: boolean;
|
||||
/** the tuning was switched off for these choices (it is kept: last_active can be switched on again) */
|
||||
switched_off?: boolean;
|
||||
last_active?: TunePreset | null;
|
||||
}
|
||||
|
||||
export interface TuneProgress { stage: string; text: string | null; minutesLeft: number | null; runsDone: number; runsPlanned: number; elapsedS: number; step: string }
|
||||
@@ -42,6 +45,11 @@ export interface TuneResult {
|
||||
answers?: { class: 'exact' | 'placement' | 'nonrepeatable' | 'quality'; fields: string[]; default_repeatable?: boolean };
|
||||
preset?: { id: string; name: string } | null;
|
||||
notes?: string[];
|
||||
/** what the screen shows: faster | already_fastest | busy_pc | failed | stopped */
|
||||
code?: 'faster' | 'already_fastest' | 'busy_pc' | 'failed' | 'stopped';
|
||||
/** busy_pc: noisy | drift | not_tried; failed: start_failed | no_memory */
|
||||
reason_code?: string;
|
||||
values?: { gain_pct?: number; gain_decode_pct?: number; base_tok_s?: number | null; tuned_tok_s?: number | null; noise_pct?: number | null; drift_pct?: number | null; minutes?: number; pairs?: number; faster_pairs?: number; preset_name?: string | null };
|
||||
verdict?: 'keep' | 'retune' | 'inconclusive';
|
||||
reason?: string;
|
||||
}
|
||||
@@ -52,6 +60,11 @@ export const tuneStatus = (model: string, resourceProfile: string, vision: strin
|
||||
api.get<TuneStatus>(`/api/native/tune/status?${q({ model, resourceProfile, vision })}`);
|
||||
export const tunePresetAction = (id: string, action: 'apply' | 'rename' | 'delete' | 'undo', body: Record<string, unknown> = {}) =>
|
||||
api.post(`/api/native/tune/presets/${id}/${action}`, body);
|
||||
export const tuneOff = (model: string, resourceProfile: string, vision: string) =>
|
||||
api.post('/api/native/tune/off', { model, resourceProfile, vision });
|
||||
/** What a start will really run: the saved tuning's values on top of the configuration (Expert mode shows these). */
|
||||
export const tuneEffective = (model: string, resourceProfile: string, vision: string) =>
|
||||
api.get<{ applied: boolean; preset: { id: string; name: string } | null; args: string[]; env: Record<string, string>; changed: { flag: string; was: string | null; now: string | null }[]; why_not?: string | null }>(`/api/native/tune/effective?${q({ model, resourceProfile, vision })}`);
|
||||
export const tuneUndo = (model: string, resourceProfile: string, vision: string) =>
|
||||
api.post('/api/native/tune/undo', { model, resourceProfile, vision });
|
||||
|
||||
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "Erneut abstimmen",
|
||||
"tune36": "Abstimmung starten",
|
||||
"tune37": "Dein Modell wird abgestimmt",
|
||||
"tune38": "Auf diesem PC können die Antworten von Lauf zu Lauf abweichen, auch mit der Standardeinstellung; diese Einstellung verstärkt das nicht."
|
||||
"tune38": "Auf diesem PC können die Antworten von Lauf zu Lauf abweichen, auch mit der Standardeinstellung; diese Einstellung verstärkt das nicht.",
|
||||
"tune39": "Deine Abstimmung „{0}“ wird verwendet",
|
||||
"tune40": "Ausschalten",
|
||||
"tune41": "Einschalten",
|
||||
"tune42": "Deine Abstimmung ist aus. Es gelten die Standardeinstellungen.",
|
||||
"tune43": "Dein PC war ausgelastet, daher waren die Messungen nicht zuverlässig. Es wurde nichts geändert. Versuche es erneut, wenn er ruhig ist."
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "Tune again",
|
||||
"tune36": "Start tuning",
|
||||
"tune37": "Tuning your model",
|
||||
"tune38": "Answers can differ from run to run on this PC even with the standard setup; this setting does not add to that."
|
||||
"tune38": "Answers can differ from run to run on this PC even with the standard setup; this setting does not add to that.",
|
||||
"tune39": "Using your tuning '{0}'",
|
||||
"tune40": "Switch off",
|
||||
"tune41": "Switch on",
|
||||
"tune42": "Your tuning is off. The standard settings are used.",
|
||||
"tune43": "Your PC was busy, so the measurements were not reliable. Nothing was changed. Try again when it is quiet."
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "Ajustar de nuevo",
|
||||
"tune36": "Empezar a ajustar",
|
||||
"tune37": "Ajustando tu modelo",
|
||||
"tune38": "En este PC las respuestas pueden variar de una ejecución a otra incluso con el ajuste estándar; este ajuste no lo aumenta."
|
||||
"tune38": "En este PC las respuestas pueden variar de una ejecución a otra incluso con el ajuste estándar; este ajuste no lo aumenta.",
|
||||
"tune39": "Usando tu ajuste «{0}»",
|
||||
"tune40": "Desactivar",
|
||||
"tune41": "Activar",
|
||||
"tune42": "Tu ajuste está desactivado. Se usan los ajustes estándar.",
|
||||
"tune43": "Tu PC estaba ocupado, así que las mediciones no fueron fiables. No se cambió nada. Inténtalo de nuevo cuando esté tranquilo."
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "もう一度チューニング",
|
||||
"tune36": "チューニングを開始",
|
||||
"tune37": "モデルをチューニング中",
|
||||
"tune38": "このPCでは標準設定でも実行のたびに回答が変わることがあります。この設定がそれを増やすことはありません。"
|
||||
"tune38": "このPCでは標準設定でも実行のたびに回答が変わることがあります。この設定がそれを増やすことはありません。",
|
||||
"tune39": "あなたのチューニング「{0}」を使用中",
|
||||
"tune40": "オフにする",
|
||||
"tune41": "オンにする",
|
||||
"tune42": "チューニングはオフです。標準設定を使用します。",
|
||||
"tune43": "PCが忙しく、測定結果が信頼できませんでした。何も変更していません。PCが空いているときにもう一度お試しください。"
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "다시 최적화",
|
||||
"tune36": "최적화 시작",
|
||||
"tune37": "모델을 최적화하는 중",
|
||||
"tune38": "이 PC에서는 표준 설정에서도 실행할 때마다 답변이 달라질 수 있으며, 이 설정이 그 차이를 키우지는 않습니다."
|
||||
"tune38": "이 PC에서는 표준 설정에서도 실행할 때마다 답변이 달라질 수 있으며, 이 설정이 그 차이를 키우지는 않습니다.",
|
||||
"tune39": "내 최적화 ‘{0}’ 사용 중",
|
||||
"tune40": "끄기",
|
||||
"tune41": "켜기",
|
||||
"tune42": "최적화가 꺼져 있습니다. 표준 설정을 사용합니다.",
|
||||
"tune43": "PC가 바빠서 측정 결과를 믿을 수 없었습니다. 아무것도 변경하지 않았습니다. PC가 한가할 때 다시 시도하세요."
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "Ajustar de novo",
|
||||
"tune36": "Iniciar ajuste",
|
||||
"tune37": "Ajustando o seu modelo",
|
||||
"tune38": "Neste PC as respostas podem variar de uma execução para outra mesmo com a configuração padrão; esta configuração não aumenta isso."
|
||||
"tune38": "Neste PC as respostas podem variar de uma execução para outra mesmo com a configuração padrão; esta configuração não aumenta isso.",
|
||||
"tune39": "Usando o seu ajuste “{0}”",
|
||||
"tune40": "Desligar",
|
||||
"tune41": "Ligar",
|
||||
"tune42": "O seu ajuste está desligado. As configurações padrão são usadas.",
|
||||
"tune43": "O seu PC estava ocupado, então as medições não foram confiáveis. Nada foi alterado. Tente de novo quando ele estiver tranquilo."
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "Настроить снова",
|
||||
"tune36": "Начать настройку",
|
||||
"tune37": "Настройка вашей модели",
|
||||
"tune38": "На этом ПК ответы могут отличаться от запуска к запуску даже при стандартных настройках; эта настройка этого не усиливает."
|
||||
"tune38": "На этом ПК ответы могут отличаться от запуска к запуску даже при стандартных настройках; эта настройка этого не усиливает.",
|
||||
"tune39": "Используется ваша настройка «{0}»",
|
||||
"tune40": "Выключить",
|
||||
"tune41": "Включить",
|
||||
"tune42": "Ваша настройка выключена. Используются стандартные параметры.",
|
||||
"tune43": "Ваш ПК был занят, поэтому измерения были ненадёжными. Ничего не изменено. Повторите, когда он освободится."
|
||||
}
|
||||
@@ -36,5 +36,10 @@
|
||||
"tune35": "再次调优",
|
||||
"tune36": "开始调优",
|
||||
"tune37": "正在调优你的模型",
|
||||
"tune38": "在这台电脑上,即使使用标准设置,每次运行的回答也可能不同;此设置不会增加这种差异。"
|
||||
"tune38": "在这台电脑上,即使使用标准设置,每次运行的回答也可能不同;此设置不会增加这种差异。",
|
||||
"tune39": "正在使用你的调优“{0}”",
|
||||
"tune40": "关闭",
|
||||
"tune41": "开启",
|
||||
"tune42": "你的调优已关闭,使用标准设置。",
|
||||
"tune43": "你的电脑当时很忙,测量结果不可靠。没有做任何改动。请在电脑空闲时再试。"
|
||||
}
|
||||
@@ -15,6 +15,7 @@ function handler(log, { result, hold = false, delayMs = 60 } = {}) {
|
||||
const out = json => { child.stdout.emit('data', Buffer.from(JSON.stringify(json))); child.exitCode = 0; child.emit('exit', 0); child.emit('close', 0); };
|
||||
if (cmd[0] === 'presets' && cmd[1] === 'status') return out({ ok: true, active: null, suggested_name: 'IQ2_XS 32K – Full speed', presets: [], state: null, reasons: [] });
|
||||
if (cmd[0] === 'presets' && cmd[1] === 'list') return out({ ok: true, presets: [{ id: 'a'.repeat(32), name: 'x' }] });
|
||||
if (cmd[0] === 'presets' && cmd[1] === 'effective') return out({ ok: true, applied: true, preset: { id: 'a'.repeat(32), name: 'IQ2_XS 32K – Work' }, args: ['--kv-resident', '65536'], changed: [{ flag: '--kv-resident', was: '32768', now: '65536' }] });
|
||||
if (cmd[0] === 'presets') return out({ ok: true, done: cmd[1] });
|
||||
if (cmd[0] === 'tune') {
|
||||
const status = arg(cmd, '--status-file');
|
||||
@@ -135,3 +136,23 @@ test('a model that is not prepared cannot be tuned', async () => {
|
||||
assert.match(job.error, /Prepare the model/);
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('Expert mode can read the tuned values and the tuning can be switched off for these choices', async () => {
|
||||
const log = [];
|
||||
const f = nativeFixture({ tuneHandler: handler(log, {}) });
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const eff = await f.control.tuneEffective({ model: 'iq2_xs', resourceProfile: 'work' });
|
||||
assert.equal(eff.applied, true);
|
||||
assert.deepEqual(eff.changed, [{ flag: '--kv-resident', was: '32768', now: '65536' }]);
|
||||
const cmd = log.find(c => c[1] === 'effective');
|
||||
const cfg = JSON.parse(readFileSync(cmd[2], 'utf8'));
|
||||
assert.equal(cfg.ui_resource_profile, 'work'); // the choices a load would make, so the right tuning is found
|
||||
await f.control.tuneOff({ model: 'iq2_xs', resourceProfile: 'work' });
|
||||
assert.deepEqual(log.at(-1).slice(0, 2), ['presets', 'off']);
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a busy PC is a plain result, not an error', () => {
|
||||
assert.match(resultSentence({ outcome: 'inconclusive', code: 'busy_pc', message: 'x' }), /PC was busy/);
|
||||
});
|
||||
+30
-17
@@ -7126,6 +7126,34 @@ def lift_reasoning_effort_arg(cfg: dict) -> None:
|
||||
SAMPLING_ALIASES = {"repeat_penalty": "repetition_penalty", "repeat_last_n": "penalty_last_n"}
|
||||
|
||||
|
||||
def apply_saved_tuning(cfg: dict, *, say=None) -> dict:
|
||||
"""The run config with the active saved tuning for its choices put on top (strata_control/presets.py), with no measurement.
|
||||
Every start path runs this (the Terminal, `runtime start`, the app's load, a restart of the app, an unload and a load), so the
|
||||
tuning is applied every time and never re-measured. A tune's own worker sets STRATA_NO_PRESET so that it measures the
|
||||
configuration it was given. Never fails a start: a store that cannot be read means the configuration as it is."""
|
||||
if os.environ.get("STRATA_NO_PRESET"):
|
||||
return cfg
|
||||
say = say or (lambda line: print(line, flush=True))
|
||||
try:
|
||||
from strata_control import presets as _presets
|
||||
out, applied = _presets.overlay(cfg)
|
||||
if applied.get("applied"):
|
||||
changes = {**(applied.get("args") or {}), **(applied.get("env") or {})}
|
||||
note = applied.get("notice") or ""
|
||||
say(f"[strata] saved tuning '{applied['preset']['name']}' applied: "
|
||||
f"{' '.join(f'{k} {v}' for k, v in changes.items()) or 'the standard settings (nothing to change)'}"
|
||||
f"{'; ' + note if note and 'identical' not in note else ''}")
|
||||
if applied.get("state") == "soft":
|
||||
say("[strata] this PC or engine changed since it was saved (" + "; ".join(applied["reasons"]) +
|
||||
"): a short re-check is available (strata presets recheck)")
|
||||
elif applied.get("why_not"):
|
||||
say(f"[strata] saved tuning '{applied['preset']['name']}' {applied['why_not']}; re-tune with strata tune auto")
|
||||
return out
|
||||
except Exception as e: # noqa: BLE001
|
||||
say(f"[strata] saved tuning could not be read ({type(e).__name__}); starting with the configuration as it is")
|
||||
return cfg
|
||||
|
||||
|
||||
def sampling_defaults_from_config(cfg: dict) -> dict:
|
||||
"""The run config's optional `sampling` block: defaults for the sampling fields a request leaves out, so
|
||||
a plain client gets configured sampling instead of greedy. Supported: temperature, top_p, top_k, min_p,
|
||||
@@ -7413,23 +7441,8 @@ def main(argv=None, *, lifecycle_event=None, interrupt_exit_code: int = 0) -> in
|
||||
ap.error("--shutdown-timeout must be in 0..300 seconds")
|
||||
config_bytes = Path(a.config).read_bytes() if a.config else b""
|
||||
cfg = json.loads(config_bytes.decode("utf-8-sig")) if a.config else {} # Notepad adds a BOM
|
||||
if a.config and a.engine == "strata" and not os.environ.get("STRATA_NO_PRESET"):
|
||||
# the saved tuning for this model and these choices (strata_control/presets.py): applied with no measurement. A tune's
|
||||
# own worker sets STRATA_NO_PRESET so that it measures the configuration it was given. Never fails the start.
|
||||
try:
|
||||
from strata_control import presets as _presets
|
||||
cfg, _applied = _presets.overlay(cfg)
|
||||
if _applied.get("applied"):
|
||||
print(f"[strata] saved tuning '{_applied['preset']['name']}' applied: "
|
||||
f"{' '.join(f'{k} {v}' for k, v in {**(_applied.get('args') or {}), **(_applied.get('env') or {})}.items())}"
|
||||
f"{'; ' + _applied['notice'] if _applied.get('notice') and 'identical' not in _applied['notice'] else ''}", flush=True)
|
||||
if _applied.get("state") == "soft":
|
||||
print("[strata] this PC or engine changed since it was saved (" + "; ".join(_applied["reasons"]) +
|
||||
"): a short re-check is available (strata presets recheck)", flush=True)
|
||||
elif _applied.get("why_not"):
|
||||
print(f"[strata] saved tuning '{_applied['preset']['name']}' {_applied['why_not']}; re-tune with strata tune auto", flush=True)
|
||||
except Exception as _e: # noqa: BLE001
|
||||
print(f"[strata] saved tuning could not be read ({type(_e).__name__}); starting with the configuration as it is", flush=True)
|
||||
if a.config and a.engine == "strata":
|
||||
cfg = apply_saved_tuning(cfg)
|
||||
attestation_key = os.environ.pop("STRATA_OWNER_ATTEST_KEY", None)
|
||||
launch_attestation = None
|
||||
if a.owner_fd is not None:
|
||||
|
||||
@@ -74,6 +74,10 @@ def add_presets(sp: argparse._SubParsersAction) -> None:
|
||||
ex.add_argument("--file", default=None)
|
||||
im = common(psp.add_parser("import", help="read a preset from a file"))
|
||||
im.add_argument("file")
|
||||
ef = common(psp.add_parser("effective", help="what a start with these choices will actually run: the saved tuning's values on top of the configuration"))
|
||||
ef.add_argument("target", help="a saved profile name or a run configuration (JSON)")
|
||||
of = common(psp.add_parser("off", help="switch the saved tuning off for these choices (back to the standard settings; it is kept and can be switched on again)"))
|
||||
of.add_argument("target", help="a saved profile name or a run configuration (JSON)")
|
||||
rc = common(psp.add_parser("recheck", help="a short check (about 2 minutes) that a preset is still as fast"))
|
||||
rc.add_argument("preset")
|
||||
rc.add_argument("target", help="the profile name or run configuration the preset belongs to")
|
||||
@@ -244,6 +248,9 @@ def auto(a: argparse.Namespace) -> int:
|
||||
except presets.PresetError as e:
|
||||
result.notes.append(str(e))
|
||||
out["preset"] = ({"id": saved["id"], "name": saved["name"]} if saved else None)
|
||||
autotune.Tuner.finalize(result)
|
||||
out.update({"code": result.code, "reason_code": result.reason_code, "values": result.values})
|
||||
out["values"]["preset_name"] = saved["name"] if saved else None
|
||||
state.update({"state": "done", "result": out, "stage": "done"})
|
||||
_write_status(a.status_file, state)
|
||||
if machine:
|
||||
@@ -292,6 +299,10 @@ def presets_command(a: argparse.Namespace) -> int:
|
||||
return _print({"presets": [_summary(p) for p in items]}, machine, human)
|
||||
if cmd == "status":
|
||||
return _status(a, machine)
|
||||
if cmd == "effective":
|
||||
return _effective(a, machine)
|
||||
if cmd == "off":
|
||||
return _off(a, machine)
|
||||
if cmd == "show":
|
||||
p = presets.find(a.preset)
|
||||
return _print({"preset": p}, machine, json.dumps(p, indent=2, ensure_ascii=False))
|
||||
@@ -346,6 +357,36 @@ def _summary(p: dict[str, Any]) -> dict[str, Any]:
|
||||
"recheck": p.get("recheck"), "slot": p.get("slot")}
|
||||
|
||||
|
||||
def _effective(a: argparse.Namespace, machine: bool) -> int:
|
||||
"""The values a start will really use (owner feedback, item 11): the configuration with the active saved tuning put on top,
|
||||
plus the list of what the tuning changed, so Expert mode can show the tuned numbers and not the untuned ones."""
|
||||
from strata_control import presets, tune_knobs
|
||||
_path, cfg = _load_target(a.target)
|
||||
merged, info = presets.overlay(cfg)
|
||||
before = [str(x) for x in cfg.get("args") or []]
|
||||
after = [str(x) for x in merged.get("args") or []]
|
||||
changed = []
|
||||
for flag in sorted({x for x in before + after if x.startswith("--")}):
|
||||
was, now = tune_knobs._current(before, flag), tune_knobs._current(after, flag)
|
||||
if was != now:
|
||||
changed.append({"flag": flag, "was": was, "now": now})
|
||||
payload = {"applied": bool(info.get("applied")), "preset": info.get("preset"), "state": info.get("state"), "reasons": info.get("reasons", []),
|
||||
"why_not": info.get("why_not"), "notice": info.get("notice"), "args": merged.get("args"), "env": merged.get("env") or {},
|
||||
"sampling": merged.get("sampling") or {}, "changed": changed, "per_request": info.get("per_request") or {}}
|
||||
human = (f"Using your tuning '{info['preset']['name']}'" if info.get("applied") else "No saved tuning is applied; the standard settings run.")
|
||||
return _print(payload, machine, human)
|
||||
|
||||
|
||||
def _off(a: argparse.Namespace, machine: bool) -> int:
|
||||
"""Switch the saved tuning off for these choices. It is kept (and listed), so switching it back on is one step."""
|
||||
from strata_control import presets
|
||||
_path, cfg = _load_target(a.target)
|
||||
ident = presets.current_identity(cfg)
|
||||
slot = presets.slot_key(presets.choices_from_config(cfg), (ident.get("model") or {}).get("sha256_shard1"))
|
||||
presets.deactivate_slot(slot)
|
||||
return _print({"active": None}, machine, "Back to the standard settings. Your tuning is kept and can be switched on again.")
|
||||
|
||||
|
||||
def _status(a: argparse.Namespace, machine: bool) -> int:
|
||||
from strata_control import presets
|
||||
_path, cfg = _load_target(a.target)
|
||||
@@ -360,7 +401,9 @@ def _status(a: argparse.Namespace, machine: bool) -> int:
|
||||
return _print({"slot": slot, "suggested_name": default, "choices": choices,
|
||||
"active": _summary(active) if active else None, "state": stale["state"] if stale else None,
|
||||
"reasons": stale["reasons"] if stale else [], "presets": [_summary(p) for p in items],
|
||||
"can_undo": bool(idx.get("history")) or bool(active)},
|
||||
"can_undo": bool(idx.get("history")) or bool(active),
|
||||
"switched_off": bool(not active and idx.get("history")),
|
||||
"last_active": next((_summary(p) for p in items if p["id"] == (idx.get("history") or [None])[-1]), None) if not active else None},
|
||||
machine, (f"Active: '{active['name']}' ({stale['state']})" if active else "No saved tuning for these choices yet."))
|
||||
|
||||
|
||||
|
||||
@@ -95,6 +95,13 @@ class ArmReport:
|
||||
@dataclass
|
||||
class TuneResult:
|
||||
outcome: str = "default_confirmed" # improved | default_confirmed | inconclusive | base_failed | cancelled
|
||||
#: what a screen shows, one of: faster | already_fastest | busy_pc | failed | stopped (see `finalize`)
|
||||
code: str = "already_fastest"
|
||||
#: why, for busy_pc: noisy | drift | not_tried (no memory or time to compare anything); for failed: start_failed | no_engine
|
||||
reason_code: str = ""
|
||||
#: the numbers a screen puts in its sentence, all plain numbers: gain_pct, base_tok_s, tuned_tok_s, noise_pct, drift_pct,
|
||||
#: minutes, pairs, faster_pairs, preset_name
|
||||
values: dict[str, Any] = field(default_factory=dict)
|
||||
message: str = ""
|
||||
mode: str = "quick"
|
||||
seconds: float = 0.0
|
||||
@@ -321,8 +328,25 @@ class Tuner:
|
||||
r.outcome, r.message = "cancelled", "Stopped. Your settings were not changed."
|
||||
r.seconds = round(self.clock() - self.started, 1)
|
||||
r.waited_s = round(self.waited_s, 1)
|
||||
self.finalize(r)
|
||||
return r
|
||||
|
||||
@staticmethod
|
||||
def finalize(r: "TuneResult") -> None:
|
||||
"""The structured outcome a screen shows inside the tune dialog (owner feedback, item 13): a code and plain numbers, so no
|
||||
screen has to read a sentence. faster / already_fastest / busy_pc (noisy, drifting, or nothing could be compared) /
|
||||
failed / stopped. `busy_pc` is a result, not an error: nothing was changed and trying again later is the answer."""
|
||||
r.code = {"improved": "faster", "default_confirmed": "already_fastest", "inconclusive": "busy_pc",
|
||||
"base_failed": "failed", "cancelled": "stopped"}.get(r.outcome, "failed")
|
||||
if r.outcome == "default_confirmed" and r.noise.get("noisy"):
|
||||
r.reason_code = r.reason_code or "noisy_but_nothing_better"
|
||||
base = (r.base or {}).get("decode_tok_s")
|
||||
tuned = (r.chosen or {}).get("decode_tok_s")
|
||||
r.values = {"gain_pct": r.gain_pct, "gain_decode_pct": r.gain_decode_pct, "base_tok_s": base, "tuned_tok_s": tuned,
|
||||
"noise_pct": round(100 * (r.noise or {}).get("spread", 0.0), 1) if r.noise else None,
|
||||
"drift_pct": round(100 * (r.noise or {}).get("drift", 0.0), 1) if r.noise else None,
|
||||
"minutes": round(r.seconds / 60.0 - r.waited_s / 60.0, 1), "pairs": r.pairs, "faster_pairs": r.faster_pairs}
|
||||
|
||||
def _spec(self) -> RequestSet:
|
||||
long_tokens = 0
|
||||
if self.ctx.context_tokens >= 65536:
|
||||
@@ -343,6 +367,7 @@ class Tuner:
|
||||
why = self._ram_guard(0, "your current settings")
|
||||
if why:
|
||||
r.outcome, r.message = "base_failed", "Not tuned: " + why
|
||||
r.reason_code = "no_memory"
|
||||
return
|
||||
pr_rows: list[dict[str, Any]] = []
|
||||
|
||||
@@ -355,6 +380,7 @@ class Tuner:
|
||||
self.base_runs.append(base1)
|
||||
if base1.status != "PASS" or not metric(base1, o.use):
|
||||
r.outcome = "base_failed"
|
||||
r.reason_code = "start_failed"
|
||||
r.message = "Your current settings could not be measured: " + (base1.reason or base1.status)
|
||||
return
|
||||
r.base = base1.to_dict()
|
||||
@@ -574,6 +600,7 @@ class Tuner:
|
||||
chosen_args, chosen_env, winners, final_pairs = {}, {}, [], []
|
||||
per_request_chosen = {}
|
||||
r.outcome = "inconclusive"
|
||||
r.reason_code = "drift"
|
||||
r.message = ("The PC changed speed while measuring (about %.0f%%), more than the gain found, so nothing "
|
||||
"was changed. Try again when nothing else is running." % (drift * 100))
|
||||
if r.outcome != "inconclusive":
|
||||
@@ -581,10 +608,12 @@ class Tuner:
|
||||
r.outcome = "improved"
|
||||
elif r.noise["noisy"] and (spread > 2 * STAGE_GAP_NOISY):
|
||||
r.outcome = "inconclusive"
|
||||
r.reason_code = "noisy"
|
||||
r.message = "The PC was too noisy to tell settings apart, so nothing was changed. Try again when it is idle."
|
||||
elif arms and not any(a.status in ("tested", "dropped", "failed") for a in arms) and not r.per_request:
|
||||
# candidates were planned but none could be measured (no free memory, no time): that is not "already the fastest"
|
||||
r.outcome = "inconclusive"
|
||||
r.reason_code = "not_tried"
|
||||
r.message = ("Other settings could not be tried (not enough free memory or time), so nothing was compared and nothing was "
|
||||
"changed. Try again when the PC is quiet.")
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""A saved tuning is applied at every start: after an app restart (a new process), after an unload and a load, and it can be seen
|
||||
(`presets effective`, `presets status`) and switched off and on again."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def cli(env, *args):
|
||||
out = subprocess.run([sys.executable, str(ROOT / "strata.py"), *args, "--output", "json"], capture_output=True, text=True,
|
||||
env=env, cwd=str(ROOT), timeout=120)
|
||||
return json.loads(out.stdout[out.stdout.index("{"):]), out
|
||||
|
||||
|
||||
def fresh_process(env, code):
|
||||
"""A new Python process: what an app restart or a new server start is."""
|
||||
r = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, env=env, cwd=str(ROOT), timeout=120)
|
||||
assert r.returncode == 0, r.stderr
|
||||
return json.loads(r.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
class PersistenceTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
t = Path(self.tmp.name)
|
||||
(t / "model.gguf").write_bytes(b"gguf" * 1000)
|
||||
(t / "engine.exe").write_bytes(b"x")
|
||||
(t / "tok").mkdir()
|
||||
self.cfg_path = t / "strata-model.json"
|
||||
self.cfg = {"exe": str(t / "engine.exe"), "tokenizer": str(t / "tok"), "model_name": "iq3_s-test", "ui_resource_profile": "work",
|
||||
"args": ["--native", str(t / "model.gguf"), "--expert-cache", "auto", "--spec", "4", "--max-context", "65536",
|
||||
"--kv", "int8", "--kv-resident", "32768"]}
|
||||
self.cfg_path.write_text(json.dumps(self.cfg))
|
||||
self.env = dict(os.environ, STRATA_PRESET_DIR=str(t / "presets"), PYTHONPATH=str(ROOT), STRATA_NO_PRESET="")
|
||||
self.env.pop("STRATA_NO_PRESET")
|
||||
# the tune's result, saved as a preset by the library, the way `strata tune auto` does
|
||||
sys.path.insert(0, str(ROOT))
|
||||
os.environ["STRATA_PRESET_DIR"] = str(t / "presets")
|
||||
self.addCleanup(lambda: os.environ.pop("STRATA_PRESET_DIR", None))
|
||||
from strata_control import presets
|
||||
self.presets = presets
|
||||
ident = presets.current_identity(self.cfg, exe=self.cfg["exe"])
|
||||
result = {"outcome": "improved", "seconds": 900, "settings": {"args": {"--kv-resident": 65536}, "env": {}, "per_request": {}},
|
||||
"answers": {"class": "exact", "fields": []}, "base": {"decode_tok_s": 60.0}, "chosen": {"decode_tok_s": 64.0},
|
||||
"gain_pct": 6.6, "pairs": 2, "faster_pairs": 2, "noise": {}}
|
||||
self.preset = presets.create(result, choices=presets.choices_from_config(self.cfg), identity=ident, name="IQ3_S 64K – Work")
|
||||
|
||||
CODE = ("import json,sys;sys.path.insert(0,'.');from serve import server;"
|
||||
"c=json.load(open(%r));out=server.apply_saved_tuning(c,say=lambda l:None);"
|
||||
"print(json.dumps(out['args']))")
|
||||
|
||||
def args_at_start(self):
|
||||
return fresh_process(self.env, self.CODE % str(self.cfg_path))
|
||||
|
||||
def test_applied_after_restart_and_after_unload_and_load(self):
|
||||
for _ in range(3): # three starts = a restart, an unload, a load: every one is a new server process
|
||||
args = self.args_at_start()
|
||||
self.assertEqual(args[args.index("--kv-resident") + 1], "65536")
|
||||
self.assertEqual(args[args.index("--spec") + 1], "4") # the user's own flags are untouched
|
||||
self.assertEqual(json.loads(self.cfg_path.read_text())["args"], self.cfg["args"]) # the saved run config itself never changes
|
||||
|
||||
def test_a_tune_worker_measures_the_configuration_as_given(self):
|
||||
env = dict(self.env, STRATA_NO_PRESET="1")
|
||||
args = fresh_process(env, self.CODE % str(self.cfg_path))
|
||||
self.assertEqual(args[args.index("--kv-resident") + 1], "32768")
|
||||
|
||||
def test_expert_mode_can_read_what_will_run(self):
|
||||
out, _ = cli(self.env, "presets", "effective", str(self.cfg_path))
|
||||
self.assertTrue(out["applied"])
|
||||
self.assertEqual(out["preset"]["name"], "IQ3_S 64K – Work")
|
||||
self.assertEqual(out["changed"], [{"flag": "--kv-resident", "was": "32768", "now": "65536"}])
|
||||
self.assertEqual(out["args"][out["args"].index("--kv-resident") + 1], "65536")
|
||||
|
||||
def test_which_tuning_is_in_use_and_switching_it_off_and_on(self):
|
||||
st, _ = cli(self.env, "presets", "status", str(self.cfg_path))
|
||||
self.assertEqual(st["active"]["name"], "IQ3_S 64K – Work")
|
||||
self.assertFalse(st["switched_off"])
|
||||
off, _ = cli(self.env, "presets", "off", str(self.cfg_path))
|
||||
self.assertIsNone(off["active"])
|
||||
args = self.args_at_start()
|
||||
self.assertEqual(args[args.index("--kv-resident") + 1], "32768") # back to the standard settings
|
||||
eff, _ = cli(self.env, "presets", "effective", str(self.cfg_path))
|
||||
self.assertFalse(eff["applied"])
|
||||
st, _ = cli(self.env, "presets", "status", str(self.cfg_path))
|
||||
self.assertIsNone(st["active"])
|
||||
self.assertTrue(st["switched_off"])
|
||||
self.assertEqual(st["last_active"]["name"], "IQ3_S 64K – Work") # kept, and listed, so it can be switched on again
|
||||
on, _ = cli(self.env, "presets", "apply", st["last_active"]["id"])
|
||||
self.assertTrue(on["ok"])
|
||||
args = self.args_at_start()
|
||||
self.assertEqual(args[args.index("--kv-resident") + 1], "65536")
|
||||
|
||||
def test_a_changed_gpu_is_not_applied_and_says_why(self):
|
||||
p = self.presets.load(self.preset["id"])
|
||||
p["identity"]["hardware"]["hard"]["gpus"] = [{"name": "Another GPU", "vram_mib": 1}]
|
||||
self.presets._write_json(self.presets._path(p["id"]), p)
|
||||
out, _ = cli(self.env, "presets", "effective", str(self.cfg_path))
|
||||
self.assertFalse(out["applied"])
|
||||
self.assertIn("graphics card", out["why_not"])
|
||||
|
||||
|
||||
class OutcomeCodesTest(unittest.TestCase):
|
||||
def test_codes_and_values(self):
|
||||
from strata_control import autotune
|
||||
mk = autotune.TuneResult
|
||||
r = mk(outcome="improved", gain_pct=7.2, seconds=1200, waited_s=120, pairs=5, faster_pairs=4)
|
||||
r.base, r.chosen = {"decode_tok_s": 50.0}, {"decode_tok_s": 53.6}
|
||||
autotune.Tuner.finalize(r)
|
||||
self.assertEqual(r.code, "faster")
|
||||
self.assertEqual(r.values["gain_pct"], 7.2)
|
||||
self.assertEqual(r.values["minutes"], 18.0)
|
||||
for outcome, reason, code in (("default_confirmed", "", "already_fastest"), ("inconclusive", "noisy", "busy_pc"),
|
||||
("inconclusive", "drift", "busy_pc"), ("inconclusive", "not_tried", "busy_pc"),
|
||||
("base_failed", "start_failed", "failed"), ("cancelled", "", "stopped")):
|
||||
x = mk(outcome=outcome, reason_code=reason)
|
||||
autotune.Tuner.finalize(x)
|
||||
self.assertEqual((x.code, x.reason_code), (code, reason), outcome)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user