From 5533043b0276d4013e1fc245616080c1b949f4ef Mon Sep 17 00:00:00 2001 From: Max Gorog Date: Fri, 8 May 2026 16:44:23 -0500 Subject: [PATCH] live scene: per-model lanes (A100 inference), not per-host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 " 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) --- training/dashboard/static/dashboard.js | 96 ++++++++++++++------------ training/dashboard/static/index.html | 2 +- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/training/dashboard/static/dashboard.js b/training/dashboard/static/dashboard.js index 314533c..2a1507f 100644 --- a/training/dashboard/static/dashboard.js +++ b/training/dashboard/static/dashboard.js @@ -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 = ` -
${hostId}
+
${modelName}
`; 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 = `
${phaseLabel}
-
${d.host_id}
-
profile: ${d.profile ?? '—'}
-
model: ${d.model ?? '—'}${ - d.latency_ms != null ? ` · ${d.latency_ms.toFixed(0)} ms` : '' +
model: ${d.model ?? '—'}${ + d.latency_ms != null ? ` · A100 ${d.latency_ms.toFixed(1)} ms` : '' + }
+
window from: ${d.host_id ?? '—'}${ + d.profile ? ` · profile ${d.profile}` : '' }
${truthLine}
@@ -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 = '
awaiting live_detection events from the inference loop
'; + lastHost = null; + latestEl.innerHTML = '
awaiting live_detection events from the A100 inference loop
'; 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, diff --git a/training/dashboard/static/index.html b/training/dashboard/static/index.html index 17408cd..89c38ed 100644 --- a/training/dashboard/static/index.html +++ b/training/dashboard/static/index.html @@ -1314,6 +1314,6 @@
- +