live scene: per-model lanes (A100 inference), not per-host

The scene's framing was wrong. It's about the A100 doing live
model predictions, not about per-host telemetry collection. Lanes
now key on `model` instead of `host_id`; the callout leads with
model name + A100 latency, demoting host/profile to secondary
metadata. Stats line reads "N models · X infer/sec · last window
from <host>" instead of "N hosts · model: X".

Demo synthesis updated to match: 5 trained models cycle through
predictions on rotating fleet windows, each model with its own
accuracy + latency profile (KNN fast/loose, BERT slow/precise) so
the lanes visually differ. Article prose reframes the scene as
side-by-side model agreement, the natural read of per-model lanes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Max Gorog 2026-05-08 16:44:23 -05:00
parent 804220d7f6
commit 5533043b02
2 changed files with 53 additions and 45 deletions

View file

@ -2121,19 +2121,20 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
rebuildLegend();
})();
// ── Live detections (scene: live) ─────────────────────────────
// Per-host swim lanes of recent prediction cells, plus a "latest
// detection" callout. New cells push in from the right; lane caps
// at MAX_CELLS so memory stays bounded across long sessions. When
// ── A100 live inference (scene: live) ─────────────────────────
// Per-MODEL swim lanes of recent prediction cells. Each row is a
// model the A100 is running inference with; each cell is one
// ten-second-window prediction colored by predicted phase. When
// the producer also sends `actual`, mismatch cells get a hatched
// overlay and the running hit-rate updates.
// overlay and the running hit-rate updates. The "latest" callout
// below the lanes leads with the model name + predicted phase.
(function () {
const lanesEl = document.getElementById('live-lanes');
const latestEl = document.getElementById('live-latest');
const statsHosts = document.getElementById('live-stats-hosts');
const statsRate = document.getElementById('live-stats-rate');
const statsModel = document.getElementById('live-stats-model');
const statsAcc = document.getElementById('live-stats-acc');
const statsModels = document.getElementById('live-stats-hosts'); // id kept for HTML compat
const statsRate = document.getElementById('live-stats-rate');
const statsHost = document.getElementById('live-stats-model'); // id kept; meaning flipped
const statsAcc = document.getElementById('live-stats-acc');
if (!lanesEl) return;
const MAX_CELLS = 60;
@ -2142,27 +2143,27 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
const lanes = new Map();
const eventTimes = [];
let totalCorrect = 0, totalLabeled = 0;
let lastModel = null;
let lastHost = null;
function ensureLane(hostId) {
if (lanes.has(hostId)) return lanes.get(hostId);
function ensureLane(modelName) {
if (lanes.has(modelName)) return lanes.get(modelName);
const row = document.createElement('div');
row.className = 'live-lane';
row.innerHTML = `
<div class="live-lane-host" title="${hostId}">${hostId}</div>
<div class="live-lane-host" title="${modelName}">${modelName}</div>
<div class="live-lane-cells"></div>`;
lanesEl.appendChild(row);
const lane = { row, cellsEl: row.querySelector('.live-lane-cells'), cells: [] };
lanes.set(hostId, lane);
lanes.set(modelName, lane);
return lane;
}
function paintCell(d) {
const lane = ensureLane(d.host_id);
const lane = ensureLane(d.model || 'unknown');
const cell = document.createElement('div');
cell.className = `live-cell ${d.predicted}`;
if (d.actual && d.actual !== d.predicted) cell.classList.add('miss');
cell.title = `${d.host_id} · w${d.window_idx ?? '?'} · ${d.predicted}` +
cell.title = `${d.model ?? '?'} · ${d.host_id ?? '?'} w${d.window_idx ?? '?'} · ${d.predicted}` +
(d.actual ? ` (truth: ${d.actual})` : '') +
(d.confidence != null ? ` · conf ${d.confidence.toFixed(2)}` : '');
lane.cellsEl.appendChild(cell);
@ -2187,10 +2188,11 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
latestEl.innerHTML = `
<div class="live-phase-block ${d.predicted}">${phaseLabel}</div>
<div class="live-meta">
<div class="live-meta-host">${d.host_id}</div>
<div class="live-meta-line">profile: <code>${d.profile ?? '—'}</code></div>
<div class="live-meta-line">model: <code>${d.model ?? '—'}</code>${
d.latency_ms != null ? ` · ${d.latency_ms.toFixed(0)} ms` : ''
<div class="live-meta-host">model: ${d.model ?? '—'}${
d.latency_ms != null ? ` · A100 ${d.latency_ms.toFixed(1)} ms` : ''
}</div>
<div class="live-meta-line">window from: <code>${d.host_id ?? '—'}</code>${
d.profile ? ` · profile <code>${d.profile}</code>` : ''
}</div>
<div class="live-meta-line">${truthLine}</div>
</div>
@ -2204,10 +2206,10 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
const now = Date.now();
while (eventTimes.length && now - eventTimes[0] > RATE_WINDOW_MS) eventTimes.shift();
const rate = (eventTimes.length / (RATE_WINDOW_MS / 1000)).toFixed(1);
statsHosts.textContent = `${lanes.size} host${lanes.size === 1 ? '' : 's'}`;
statsRate.textContent = `${rate} / sec`;
statsModel.textContent = `model: ${lastModel ?? '—'}`;
statsAcc.textContent = totalLabeled > 0
statsModels.textContent = `${lanes.size} model${lanes.size === 1 ? '' : 's'}`;
statsRate.textContent = `${rate} infer / sec`;
statsHost.textContent = `last window: ${lastHost ?? '—'}`;
statsAcc.textContent = totalLabeled > 0
? `hit-rate: ${(100 * totalCorrect / totalLabeled).toFixed(0)}% (${totalCorrect}/${totalLabeled})`
: `hit-rate: —`;
}
@ -2224,13 +2226,13 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
lanes.clear();
eventTimes.length = 0;
totalCorrect = 0; totalLabeled = 0;
lastModel = null;
latestEl.innerHTML = '<div class="live-latest-empty">awaiting <code>live_detection</code> events from the inference loop</div>';
lastHost = null;
latestEl.innerHTML = '<div class="live-latest-empty">awaiting <code>live_detection</code> events from the A100 inference loop</div>';
updateStats();
}
function paintDetection(d) {
eventTimes.push(Date.now());
if (d.model) lastModel = d.model;
if (d.host_id) lastHost = d.host_id;
if (d.actual) {
totalLabeled++;
if (d.actual === d.predicted) totalCorrect++;
@ -2239,47 +2241,53 @@ def train_nn(*, model, X_train, y_train, X_val, y_val,
paintLatest(d);
}
function handleDetection(d) {
if (!d.host_id || !d.predicted) return;
if (!d.model || !d.predicted) return;
paintDetection(d);
}
on('live_detection', m => {
if (!m.host_id || !m.predicted) return;
if (!m.model || !m.predicted) return;
cachedReal.push(m);
if (cachedReal.length > REAL_CACHE_CAP) cachedReal.shift();
if (!demoActive) paintDetection(m);
});
// Synthetic demo: 5 hosts, walk through phases, ~92% accuracy.
// Synthetic demo: A100 cycling through 5 trained models, scoring
// windows from rotating fleet hosts. Each model has its own
// accuracy + latency profile (KNN faster but less accurate;
// BERT slower but more accurate) so the lanes look distinct.
let demoTimer = null;
function demoStart() {
demoActive = true;
clearLanes();
if (demoTimer) clearInterval(demoTimer);
const HOSTS = [
{ id: 'elliott-lab', profile: 'cpu-saturate', phaseIdx: 0 },
{ id: 'elliott-thinkpad', profile: 'ransomware-lite', phaseIdx: 0 },
{ id: 'k-gamingcom', profile: 'bursty-c2', phaseIdx: 1 },
{ id: 'smelliott', profile: 'fork-bomb', phaseIdx: 0 },
{ id: 'gosling', profile: 'crypto-miner', phaseIdx: 2 },
const MODELS = [
{ name: 'knn', acc: 0.78, latMs: 0.4, phaseIdx: 0 },
{ name: 'rnn', acc: 0.86, latMs: 1.8, phaseIdx: 0 },
{ name: 'gru', acc: 0.91, latMs: 2.4, phaseIdx: 1 },
{ name: 'lstm', acc: 0.93, latMs: 3.2, phaseIdx: 2 },
{ name: 'bert', acc: 0.95, latMs: 8.6, phaseIdx: 0 },
];
const HOSTS = ['elliott-lab', 'elliott-thinkpad', 'k-gamingcom'];
const PROFILES = ['cpu-saturate', 'ransomware-lite', 'bursty-c2', 'fork-bomb', 'crypto-miner'];
const PHASES = ['clean', 'armed', 'infecting', 'infected_running', 'dormant'];
let counter = 0;
demoTimer = setInterval(() => {
const host = HOSTS[counter % HOSTS.length];
const m = MODELS[counter % MODELS.length];
counter++;
if (Math.random() < 0.18) host.phaseIdx = (host.phaseIdx + 1) % PHASES.length;
const truth = PHASES[host.phaseIdx];
const right = Math.random() < 0.92;
if (Math.random() < 0.18) m.phaseIdx = (m.phaseIdx + 1) % PHASES.length;
const truth = PHASES[m.phaseIdx];
const right = Math.random() < m.acc;
const predicted = right
? truth
: PHASES[(host.phaseIdx + 1 + Math.floor(Math.random() * 4)) % PHASES.length];
: PHASES[(m.phaseIdx + 1 + Math.floor(Math.random() * 4)) % PHASES.length];
handleDetection({
host_id: host.id, profile: host.profile,
host_id: HOSTS[counter % HOSTS.length],
profile: PROFILES[counter % PROFILES.length],
predicted, actual: truth,
confidence: 0.62 + Math.random() * 0.36,
model: 'lstm',
latency_ms: 3 + Math.random() * 4,
model: m.name,
latency_ms: m.latMs + (Math.random() - 0.5) * m.latMs * 0.3,
episode_id: 'demo',
window_idx: counter,
t_wall: Date.now() / 1000,

View file

@ -1314,6 +1314,6 @@
</article>
</div>
<script src="/static/dashboard.js?v=2a8b8c03"></script>
<script src="/static/dashboard.js?v=48576a1a"></script>
</body>
</html>