Add barrier-confirmed yielding, measured residency, persistence and closed-loop tuning
Nine changes, in rough order of how much they affect real behaviour: 1. VRAM yield is now a barrier. Posting keep_alive:0 only asks Ollama to unload; measured here, the HTTP call returns in 63ms while the driver takes a further 77ms to release 14.9GB. Returning inside that window is how ComfyUI ends up allocating into VRAM that is still occupied. instant_free_ollama_vram() polls NVML until the allocation is actually gone and reports request/confirm split. 2. ComfyUI VRAM is no longer purged 1.5s after every prompt, which forced a full checkpoint reload on each workflow iteration. It is held for 30s of genuinely empty queue, with an immediate purge when Ollama actually asks for the memory. 3. Cache-hit classification uses achieved bandwidth (size / load duration) rather than a fixed `load_duration < 2500ms`. That constant called a 12.9GB model read at 2.9GB/s a cold load, and a 0.5GB model read from NVMe a cache hit. 4. Page-cache residency is measured, not assumed. mincore(2) reported 128GB resident on a box with 46GB of page cache: the kernel only permits page-cache introspection on files you own, and the Ollama blobs are owned by uid ollama, for which mincore answers "all resident" instead of failing. Uses cachestat(2) where permitted and a randomised read-rate probe elsewhere, labelling which was used. Fixed-offset probing was self-fulfilling, so windows are random and cold ones are returned with FADV_DONTNEED. 5. Warming is budgeted and ranked by recency/frequency instead of reading every file top-to-bottom, which on 64GB of RAM just evicts whatever was warmed first. 6. Telemetry and events persist to SQLite (~0.38 MB/hour) instead of living in a 50-entry in-memory deque, so /api/analytics/profiles can finally answer whether an overclock profile actually delivers more tok/s. 7. Thermal governor walks the overclock back on sustained heat or hardware throttling, with hysteresis, fed from the existing sampler. 8. Autotune sweeps a clock offset, benchmarks decode at each step, watches for Xid errors and degenerate output, and restores the profile in a finally block. 9. Stock clocks/power/fans are restored on shutdown and via systemd ExecStopPost. Nothing previously undid a locked clock or a manually pinned fan. Also: one shared 1Hz telemetry sampler fanned out to SSE subscribers rather than every client re-running the whole snapshot; wall-clock timestamps in place of the event loop's monotonic clock; cached nvidia-smi shell-outs; quieter httpx logging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
231
static/app.js
231
static/app.js
@@ -33,6 +33,9 @@ function initSSE() {
|
||||
function updateDashboard(data) {
|
||||
if (!data) return;
|
||||
|
||||
// Governor state rides along in the shared snapshot — no extra polling needed.
|
||||
if (data.governor) renderGovernor(data.governor);
|
||||
|
||||
// 1. GPU VRAM Stats
|
||||
const gpu = data.gpu || {};
|
||||
const ram = data.ram || {};
|
||||
@@ -646,3 +649,231 @@ async function saveOverclockProfile() {
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// THERMAL GOVERNOR / RESIDENCY / ANALYTICS / AUTOTUNE
|
||||
// ============================================================================
|
||||
|
||||
let governorEnabled = true;
|
||||
|
||||
function renderGovernor(gov) {
|
||||
if (!gov) return;
|
||||
governorEnabled = gov.enabled;
|
||||
const levels = 3;
|
||||
const el = (id) => document.getElementById(id);
|
||||
if (!el('gov-label')) return;
|
||||
|
||||
el('gov-label').textContent = gov.label || '—';
|
||||
el('gov-label').className = 'text-2xl font-bold ' +
|
||||
(gov.level === 0 ? 'text-emerald-400' : gov.level < 3 ? 'text-amber-400' : 'text-rose-400');
|
||||
el('gov-level').textContent = `level ${gov.level} / ${levels}`;
|
||||
el('gov-bar').style.width = `${(gov.level / levels) * 100}%`;
|
||||
el('gov-esc').textContent = `${gov.escalate_at_c}°C`;
|
||||
el('gov-rec').textContent = `${gov.recover_below_c}°C`;
|
||||
el('gov-scale').textContent = `${Math.round((gov.offset_scale ?? 1) * 100)}%`;
|
||||
el('gov-reason').textContent = gov.last_reason || '—';
|
||||
|
||||
const toggle = el('gov-toggle');
|
||||
toggle.textContent = gov.enabled ? 'Enabled' : 'Disabled';
|
||||
toggle.className = 'px-2.5 py-1 text-xs font-semibold rounded-lg border transition ' +
|
||||
(gov.enabled ? 'bg-emerald-950/70 border-emerald-800 text-emerald-300 hover:bg-emerald-900'
|
||||
: 'bg-slate-800 border-slate-700 text-slate-400 hover:bg-slate-700');
|
||||
|
||||
const hist = el('gov-history');
|
||||
hist.innerHTML = (gov.history || []).map(h => {
|
||||
const t = new Date(h.ts * 1000).toLocaleTimeString();
|
||||
const up = h.to_level > h.from_level;
|
||||
return `<div class="flex justify-between ${up ? 'text-rose-400' : 'text-cyan-400'}">
|
||||
<span>${t} ${up ? '▲' : '▼'} L${h.to_level} ${h.label}</span>
|
||||
<span class="text-slate-500 truncate ml-2">${h.reason || ''}</span></div>`;
|
||||
}).join('') || '<div class="text-slate-600">No derates recorded — the card has stayed happy.</div>';
|
||||
}
|
||||
|
||||
async function toggleGovernor() {
|
||||
const res = await fetch('/api/governor', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: !governorEnabled })
|
||||
});
|
||||
renderGovernor(await res.json());
|
||||
}
|
||||
|
||||
async function resetGovernor() {
|
||||
const res = await fetch('/api/governor', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reset: true })
|
||||
});
|
||||
renderGovernor(await res.json());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- residency
|
||||
|
||||
async function fetchCacheReport(refresh = false) {
|
||||
const body = document.getElementById('cache-files');
|
||||
if (!body) return;
|
||||
if (refresh) body.innerHTML = '<div class="text-slate-500 text-xs">Scanning…</div>';
|
||||
try {
|
||||
const res = await fetch(`/api/cache/report?refresh=${refresh ? 'true' : 'false'}`);
|
||||
const d = await res.json();
|
||||
|
||||
document.getElementById('cache-total').textContent =
|
||||
`${d.resident_total_gb} / ${d.catalog_total_gb} GB`;
|
||||
document.getElementById('cache-meta').textContent =
|
||||
`${d.residency_pct}% resident · ${d.warm_files}/${d.files_scanned} warm · ${d.scan_ms}ms`;
|
||||
|
||||
const cap = d.capability || {};
|
||||
document.getElementById('cache-capability').innerHTML = cap.exact_everywhere
|
||||
? '<span class="text-emerald-500">Exact measurement available for every model file.</span>'
|
||||
: `<span class="text-amber-500">Ollama weights measured by read-rate probe</span> — ${cap.reason || ''}`;
|
||||
|
||||
body.innerHTML = (d.files || []).slice(0, 40).map(f => {
|
||||
const pct = f.resident_pct ?? 0;
|
||||
const color = pct >= 90 ? 'bg-emerald-500' : pct >= 25 ? 'bg-amber-500' : 'bg-slate-700';
|
||||
const kind = f.kind === 'ollama' ? 'text-cyan-400' : 'text-fuchsia-400';
|
||||
return `<div>
|
||||
<div class="flex justify-between text-[11px] font-mono">
|
||||
<span class="${kind} truncate mr-2" title="${f.full_path}">${f.name}</span>
|
||||
<span class="text-slate-400 whitespace-nowrap">${pct}% · ${f.size_gb}GB</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-950 rounded-full h-1 mt-0.5 overflow-hidden">
|
||||
<div class="${color} h-full transition-all" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
body.innerHTML = `<div class="text-rose-400 text-xs">Residency scan failed: ${e}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWarmPlan() {
|
||||
const body = document.getElementById('cache-files');
|
||||
body.innerHTML = '<div class="text-slate-500 text-xs">Building plan…</div>';
|
||||
try {
|
||||
const d = await (await fetch('/api/warm-plan')).json();
|
||||
document.getElementById('cache-total').textContent = `${d.planned_gb} GB planned`;
|
||||
document.getElementById('cache-meta').textContent =
|
||||
`budget ${d.budget_gb} GB · ${d.warm_count} to warm · ${d.skipped_count} skipped`;
|
||||
document.getElementById('cache-capability').innerHTML =
|
||||
'<span class="text-slate-500">Ranked by recency/frequency; already-resident files cost nothing.</span>';
|
||||
body.innerHTML = d.plan.map((p, i) => `<div class="flex justify-between text-[11px] font-mono">
|
||||
<span class="text-amber-400 truncate mr-2">${i + 1}. ${p.name}</span>
|
||||
<span class="text-slate-400 whitespace-nowrap">${p.size_gb}GB · ${p.resident_pct}% · s=${p.score}</span>
|
||||
</div>`).join('') || '<div class="text-emerald-500 text-xs">Everything worth warming is already resident.</div>';
|
||||
} catch (e) {
|
||||
body.innerHTML = `<div class="text-rose-400 text-xs">${e}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- analytics
|
||||
|
||||
async function fetchAnalytics() {
|
||||
const el = document.getElementById('analytics-body');
|
||||
if (!el) return;
|
||||
const days = document.getElementById('analytics-days').value;
|
||||
try {
|
||||
const d = await (await fetch(`/api/analytics/profiles?days=${days}`)).json();
|
||||
if (!d.profiles || !d.profiles.length) {
|
||||
el.innerHTML = '<div class="text-slate-500">No swaps recorded yet in this window. Switch a model to start building the comparison.</div>';
|
||||
return;
|
||||
}
|
||||
const best = Math.max(...d.profiles.map(p => p.avg_tok_s || 0));
|
||||
el.innerHTML = `<table class="w-full text-[11px] font-mono">
|
||||
<thead><tr class="text-slate-500 text-left border-b border-slate-800">
|
||||
<th class="pb-1">profile</th><th>tok/s</th><th>load GB/s</th><th>°C avg</th><th>W avg</th><th>SM MHz</th><th>n</th>
|
||||
</tr></thead><tbody>` +
|
||||
d.profiles.map(p => {
|
||||
const win = (p.avg_tok_s || 0) === best;
|
||||
return `<tr class="border-b border-slate-900 ${win ? 'text-emerald-400' : 'text-slate-300'}">
|
||||
<td class="py-1">${win ? '★ ' : ''}${p.profile ?? '—'}</td>
|
||||
<td>${p.avg_tok_s ?? '—'}</td><td>${p.avg_load_gbps ?? '—'}</td>
|
||||
<td>${p.avg_temp_c ?? '—'}</td><td>${p.avg_power_w ?? '—'}</td>
|
||||
<td>${p.avg_clock_sm ?? '—'}</td><td class="text-slate-500">${p.swaps}</td></tr>`;
|
||||
}).join('') + '</tbody></table>';
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="text-rose-400">${e}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- autotune
|
||||
|
||||
let sweepPoll = null;
|
||||
|
||||
async function runSweep() {
|
||||
const btn = document.getElementById('at-run');
|
||||
const body = document.getElementById('autotune-body');
|
||||
const payload = {
|
||||
knob: document.getElementById('at-knob').value,
|
||||
profile: document.getElementById('at-profile').value,
|
||||
start: parseInt(document.getElementById('at-start').value, 10),
|
||||
stop: parseInt(document.getElementById('at-stop').value, 10),
|
||||
step: parseInt(document.getElementById('at-step').value, 10),
|
||||
apply_best: document.getElementById('at-apply').checked,
|
||||
};
|
||||
btn.disabled = true;
|
||||
btn.classList.add('opacity-50');
|
||||
body.innerHTML = '<div class="text-fuchsia-400">Sweeping… each step loads the model and benchmarks decode. This takes a few minutes.</div>';
|
||||
sweepPoll = setInterval(pollSweep, 2000);
|
||||
try {
|
||||
const res = await fetch('/api/autotune/sweep', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const d = await res.json();
|
||||
renderSweep(res.ok ? d : { error: d.detail });
|
||||
} catch (e) {
|
||||
body.innerHTML = `<div class="text-rose-400">${e}</div>`;
|
||||
} finally {
|
||||
clearInterval(sweepPoll);
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('opacity-50');
|
||||
document.getElementById('autotune-state').textContent = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
async function pollSweep() {
|
||||
try {
|
||||
const d = await (await fetch('/api/autotune')).json();
|
||||
const s = document.getElementById('autotune-state');
|
||||
if (d.running && d.current) {
|
||||
s.textContent = `${d.current.knob}=${d.current.value} → ${d.current.tokens_per_sec} tok/s`;
|
||||
s.className = 'text-xs font-mono text-fuchsia-400';
|
||||
}
|
||||
} catch (e) { /* transient */ }
|
||||
}
|
||||
|
||||
async function cancelSweep() {
|
||||
await fetch('/api/autotune/cancel', { method: 'POST' });
|
||||
document.getElementById('autotune-state').textContent = 'cancelling…';
|
||||
}
|
||||
|
||||
function renderSweep(d) {
|
||||
const body = document.getElementById('autotune-body');
|
||||
if (d.error) { body.innerHTML = `<div class="text-rose-400">${d.error}</div>`; return; }
|
||||
const rows = (d.table || []).map(r => `<tr class="${r.stable ? 'text-slate-300' : 'text-rose-400'} border-b border-slate-900">
|
||||
<td class="py-0.5">${r.value}</td><td>${r.tokens_per_sec}</td><td>${r.temp_c}</td>
|
||||
<td>${r.power_w ?? '—'}</td><td>${r.clock_mem_mhz ?? '—'}</td>
|
||||
<td class="text-slate-500 truncate">${r.instability || 'stable'}</td></tr>`).join('');
|
||||
const best = d.best
|
||||
? `<div class="text-emerald-400 mb-2">★ Best stable: <span class="font-mono">${d.knob}=${d.best.value}</span>
|
||||
→ ${d.best.tokens_per_sec} tok/s at ${d.best.temp_c}°C
|
||||
${d.gain_pct != null ? `<span class="text-slate-400">(${d.gain_pct > 0 ? '+' : ''}${d.gain_pct}% vs baseline)</span>` : ''}
|
||||
${d.applied_to_profile ? '<span class="text-fuchsia-400">· saved to profile</span>' : ''}</div>`
|
||||
: '<div class="text-amber-400 mb-2">No stable step produced throughput.</div>';
|
||||
const unstable = d.first_unstable
|
||||
? `<div class="text-rose-400 text-[11px] mb-2">Stopped climbing at ${d.first_unstable.value}: ${d.first_unstable.why}</div>` : '';
|
||||
body.innerHTML = best + unstable + `<table class="w-full text-[11px] font-mono">
|
||||
<thead><tr class="text-slate-500 text-left border-b border-slate-800">
|
||||
<th class="pb-1">offset</th><th>tok/s</th><th>°C</th><th>W</th><th>mem MHz</th><th>status</th>
|
||||
</tr></thead><tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- wiring
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchCacheReport(false);
|
||||
fetchAnalytics();
|
||||
setInterval(fetchAnalytics, 60000);
|
||||
setInterval(() => fetchCacheReport(false), 30000);
|
||||
fetch('/api/autotune').then(r => r.json()).then(d => {
|
||||
if (d.last_result) renderSweep(d.last_result);
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user