eka toimiva
This commit is contained in:
26
network-poc/deploy.sh
Executable file
26
network-poc/deploy.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SERVER="ubuntu@86.50.252.98"
|
||||
REMOTE_DIR="~/code/agentic-studio/network-poc"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no"
|
||||
|
||||
echo "=== Kipinä Studio Deploy ==="
|
||||
|
||||
# 1. Rakennetaan Docker-image lokaalisti
|
||||
echo "[1/4] Rakennetaan image lokaalisti..."
|
||||
docker build -f Dockerfile.prod -t kipina-agentic:latest .
|
||||
|
||||
# 2. Tallennetaan ja siirretään
|
||||
echo "[2/4] Siirretään image palvelimelle..."
|
||||
docker save kipina-agentic:latest | gzip | ssh $SSH_OPTS $SERVER "gunzip | docker load"
|
||||
|
||||
# 3. Päivitetään konfiguraatiot
|
||||
echo "[3/4] Päivitetään konfiguraatiot..."
|
||||
scp $SSH_OPTS docker-compose.prod.yml Caddyfile.prod $SERVER:$REMOTE_DIR/
|
||||
|
||||
# 4. Käynnistetään uudelleen
|
||||
echo "[4/4] Käynnistetään palvelut..."
|
||||
ssh $SSH_OPTS $SERVER "cd $REMOTE_DIR && docker compose -f docker-compose.prod.yml up -d"
|
||||
|
||||
echo "=== Valmis! https://kipina.studio ==="
|
||||
@@ -14,9 +14,7 @@ services:
|
||||
- hub
|
||||
|
||||
hub:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.prod
|
||||
image: kipina-agentic:latest
|
||||
container_name: kipina-agentic-hub
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hub"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -245,7 +245,7 @@ impl NodeDb {
|
||||
en: &serde_json::Value,
|
||||
fi: &serde_json::Value,
|
||||
overhead: f64,
|
||||
duration_ms: u64,
|
||||
duration_ms: f64,
|
||||
) {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
@@ -265,7 +265,7 @@ impl NodeDb {
|
||||
en.get("chars_per_token").and_then(|v| v.as_f64()),
|
||||
fi.get("chars_per_token").and_then(|v| v.as_f64()),
|
||||
overhead,
|
||||
duration_ms as i64,
|
||||
duration_ms,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ tr:hover td { background:#1c2333; }
|
||||
</head>
|
||||
<body>
|
||||
<h1>Kipina Admin</h1>
|
||||
<p class="sub">Node-sessiot ja tokenisointivertailut</p>
|
||||
<p class="sub">Node-sessiot ja tokenisointivertailut · <span id="admin-version" style="color:var(--accent)">-</span></p>
|
||||
|
||||
<div id="stats" class="stats-grid"></div>
|
||||
|
||||
@@ -140,6 +140,9 @@ async function load() {
|
||||
const sessions = await sessionsRes.json();
|
||||
const pairs = await pairsRes.json();
|
||||
|
||||
// Versio
|
||||
if (stats.version) document.getElementById('admin-version').textContent = 'v' + stats.version;
|
||||
|
||||
// Stats
|
||||
document.getElementById('stats').innerHTML = [
|
||||
{v: stats.total_sessions, l: 'Sessioita'},
|
||||
@@ -194,7 +197,7 @@ async function load() {
|
||||
}
|
||||
|
||||
load();
|
||||
setInterval(load, 10000);
|
||||
setInterval(load, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>"##;
|
||||
@@ -275,7 +278,7 @@ async fn main() {
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
||||
tracing::debug!("Kipinä Agent Hub käynnistyy osoitteessa http://localhost:3000");
|
||||
tracing::info!("Kipinä Agent Hub v{} käynnistyy osoitteessa http://localhost:3000", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
|
||||
@@ -296,7 +299,9 @@ async fn api_pairs(
|
||||
async fn api_stats(
|
||||
axum::extract::State(state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
axum::Json(state.db.get_stats())
|
||||
let mut stats = state.db.get_stats();
|
||||
stats.as_object_mut().unwrap().insert("version".to_string(), serde_json::json!(env!("CARGO_PKG_VERSION")));
|
||||
axum::Json(stats)
|
||||
}
|
||||
|
||||
async fn admin_page() -> impl IntoResponse {
|
||||
@@ -358,6 +363,7 @@ async fn broadcast_stats(state: &Arc<AppState>) {
|
||||
let completed = *state.total_tasks.lock().unwrap();
|
||||
let stats_msg = serde_json::json!({
|
||||
"type": "stats",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"nodes": total_nodes,
|
||||
"vram_gb": total_vram,
|
||||
"tasks": completed
|
||||
@@ -553,7 +559,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
let en = obj.get("en").unwrap_or(&empty);
|
||||
let fi = obj.get("fi").unwrap_or(&empty);
|
||||
let overhead = obj.get("overhead_pct").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let duration = obj.get("duration_ms").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let duration = obj.get("duration_ms").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
|
||||
let en_text = en.get("text").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let en_tokens = en.get("token_count").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
@@ -568,7 +574,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
|
||||
let overhead_color = if overhead > 10.0 { "\x1b[31m" } else if overhead < -10.0 { "\x1b[32m" } else { "\x1b[33m" };
|
||||
|
||||
println!();
|
||||
println!("\x1b[36m━━━ Solmu {} ━━━ {}ms ━━━\x1b[0m", node_id, duration);
|
||||
println!("\x1b[36m━━━ Solmu {} ━━━ {:.2}ms ━━━\x1b[0m", node_id, duration);
|
||||
println!(" \x1b[34mEN\x1b[0m \"{}\"", en_text);
|
||||
println!(" {} merkkiä → \x1b[35m{} tokenia\x1b[0m | \x1b[32m{:.2} merkkiä/token\x1b[0m", en_chars, en_tokens, en_cpt);
|
||||
println!(" \x1b[33mFI\x1b[0m \"{}\"", fi_text);
|
||||
|
||||
@@ -15,6 +15,7 @@ web-sys = { version = "0.3.68", features = [
|
||||
"HtmlElement",
|
||||
"WebSocket",
|
||||
"MessageEvent",
|
||||
"Performance",
|
||||
"console",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -117,10 +117,11 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
||||
return;
|
||||
};
|
||||
|
||||
let start_time = js_sys::Date::now();
|
||||
let perf = web_sys::window().unwrap().performance().unwrap();
|
||||
let start_time = perf.now();
|
||||
let en_result = tokenize_text(&tokenizer, &en_text);
|
||||
let fi_result = tokenize_text(&tokenizer, &fi_text);
|
||||
let duration = (js_sys::Date::now() - start_time) as u64;
|
||||
let duration_ms = perf.now() - start_time; // millisekunteja desimaalitarkkuudella
|
||||
|
||||
let en_cpt = en_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
let fi_cpt = fi_result["chars_per_token"].as_f64().unwrap_or(0.0);
|
||||
@@ -132,15 +133,15 @@ async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<We
|
||||
((fi_tokens as f64 / en_tokens as f64) - 1.0) * 100.0
|
||||
} else { 0.0 };
|
||||
|
||||
console_log!("EN: {} tokenia ({:.2} m/t) vs FI: {} tokenia ({:.2} m/t) | ylikustannus: {:.0}%",
|
||||
en_tokens, en_cpt, fi_tokens, fi_cpt, overhead_pct);
|
||||
console_log!("EN: {} tokenia ({:.2} m/t) vs FI: {} tokenia ({:.2} m/t) | ylikustannus: {:.0}% | {:.2}ms",
|
||||
en_tokens, en_cpt, fi_tokens, fi_cpt, overhead_pct, duration_ms);
|
||||
|
||||
let pair_done = serde_json::json!({
|
||||
"type": "pair_done",
|
||||
"en": en_result,
|
||||
"fi": fi_result,
|
||||
"overhead_pct": (overhead_pct * 10.0).round() / 10.0,
|
||||
"duration_ms": duration,
|
||||
"duration_ms": (duration_ms * 100.0).round() / 100.0,
|
||||
"tokenizer": "Qwen2.5-Coder-0.5B",
|
||||
});
|
||||
let _ = ws.borrow().send_with_str(&pair_done.to_string());
|
||||
|
||||
@@ -129,6 +129,40 @@
|
||||
.btn:hover { background-color: #2ea043; }
|
||||
.hidden { display: none; }
|
||||
|
||||
.compat-banner {
|
||||
border-radius: 6px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
display: none;
|
||||
}
|
||||
.compat-banner.gpu {
|
||||
background: #23392020;
|
||||
border: 1px solid #3fb95040;
|
||||
color: var(--success-color);
|
||||
}
|
||||
.compat-banner.cpu {
|
||||
background: #d2992215;
|
||||
border: 1px solid #d2992240;
|
||||
color: #d29922;
|
||||
}
|
||||
.compat-banner code {
|
||||
background: #0d1117;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.compat-banner summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.compat-banner details[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
background-color: var(--panel-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -191,12 +225,30 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-tokens:hover { color: var(--text-color); border-color: #8b949e; }
|
||||
|
||||
.metric-card {
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.metric-val {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Kipinä <span>Agent Dashboard</span></h1>
|
||||
<p class="sub">Hajautettu WebGPU Laskentaverkko</p>
|
||||
<p class="sub">Hajautettu WebGPU Laskentaverkko · <span id="hub-version" style="color:#58a6ff">-</span></p>
|
||||
|
||||
<!-- Global Cluster Statistics (UI) -->
|
||||
<div class="dashboard-panel">
|
||||
@@ -215,16 +267,51 @@
|
||||
</div>
|
||||
|
||||
<div id="device-info" class="device-info"></div>
|
||||
<div id="compat-banner" class="compat-banner"></div>
|
||||
|
||||
<div id="initial-state">
|
||||
<button id="start-btn" class="btn">Liity laskentaverkkoon</button>
|
||||
</div>
|
||||
|
||||
<div id="active-state" class="hidden">
|
||||
<div class="slider-container">
|
||||
<label for="gpu-load">Oman Laitteen Kuormitusrajoitin: <strong id="load-display" style="color:var(--accent-color);">50%</strong></label>
|
||||
<input type="range" id="gpu-load" min="0" max="75" value="50">
|
||||
<p style="font-size: 11px; color:#8b949e;">Hallitsee "Duty Cyclea" – kuinka pitkään Wasm-ydin pakotetaan nukkumaan Tensorimatriisien laskennan välissä.</p>
|
||||
<!-- Resurssipaneeli -->
|
||||
<div style="background:#0d1117;border:1px solid var(--border-color);border-radius:6px;padding:16px;margin-bottom:16px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<span style="font-weight:600;font-size:15px">Resurssien hallinta</span>
|
||||
<span id="node-status" style="font-size:12px;color:var(--success-color)">Aktiivinen</span>
|
||||
</div>
|
||||
|
||||
<!-- Kuormitussäädin -->
|
||||
<div style="margin-bottom:14px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">
|
||||
<span>Laskentatehon rajoitin</span>
|
||||
<strong id="load-display" style="color:var(--accent-color)">50%</strong>
|
||||
</div>
|
||||
<input type="range" id="gpu-load" min="0" max="100" value="50" style="width:100%;accent-color:var(--accent-color)">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:2px">
|
||||
<span>Pysäytetty</span><span>Säästö</span><span>Tasapaino</span><span>Suorituskyky</span><span>Maksimi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reaaliaikaiset metriikat -->
|
||||
<div id="metrics-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px">
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-tasks">0</div>
|
||||
<div class="metric-label">Tehtäviä</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-avg-time">-</div>
|
||||
<div class="metric-label">Ka. aika</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-tokens">0</div>
|
||||
<div class="metric-label">Tokeneita</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val" id="m-uptime">0s</div>
|
||||
<div class="metric-label">Käynnissä</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-box" class="chat-box hidden">
|
||||
@@ -251,6 +338,29 @@
|
||||
|
||||
let currentChatMsg = null;
|
||||
|
||||
// Reaaliaikaiset metriikat
|
||||
const metrics = {
|
||||
tasks: 0,
|
||||
totalTokens: 0,
|
||||
totalTimeMs: 0,
|
||||
startTime: null,
|
||||
};
|
||||
|
||||
function updateMetrics() {
|
||||
document.getElementById('m-tasks').textContent = metrics.tasks;
|
||||
document.getElementById('m-tokens').textContent = metrics.totalTokens.toLocaleString('fi-FI');
|
||||
document.getElementById('m-avg-time').textContent = metrics.tasks > 0
|
||||
? (metrics.totalTimeMs / metrics.tasks).toFixed(1) + 'ms'
|
||||
: '-';
|
||||
if (metrics.startTime) {
|
||||
const sec = Math.floor((Date.now() - metrics.startTime) / 1000);
|
||||
if (sec < 60) document.getElementById('m-uptime').textContent = sec + 's';
|
||||
else if (sec < 3600) document.getElementById('m-uptime').textContent = Math.floor(sec/60) + 'min';
|
||||
else document.getElementById('m-uptime').textContent = Math.floor(sec/3600) + 'h ' + (Math.floor(sec/60)%60) + 'min';
|
||||
}
|
||||
}
|
||||
setInterval(updateMetrics, 1000);
|
||||
|
||||
// Ylikirjoitetaan console.log uppoamaan lokilaatikkoon
|
||||
const originalLog = console.log;
|
||||
console.log = function(...args) {
|
||||
@@ -272,9 +382,22 @@
|
||||
|
||||
// UI Slider Listener -> Lähettää arvon suoraan WebAssemblyn ytimeen!
|
||||
loadSlider.addEventListener('input', (e) => {
|
||||
loadDisplay.textContent = e.target.value + '%';
|
||||
const val = parseInt(e.target.value);
|
||||
loadDisplay.textContent = val + '%';
|
||||
if (window.wasm_active) {
|
||||
set_gpu_load(parseInt(e.target.value));
|
||||
set_gpu_load(val);
|
||||
}
|
||||
// Tilapäivitys
|
||||
const statusEl = document.getElementById('node-status');
|
||||
if (val === 0) {
|
||||
statusEl.textContent = 'Pysäytetty';
|
||||
statusEl.style.color = '#f85149';
|
||||
} else if (val <= 25) {
|
||||
statusEl.textContent = 'Säästötila';
|
||||
statusEl.style.color = '#d29922';
|
||||
} else {
|
||||
statusEl.textContent = 'Aktiivinen';
|
||||
statusEl.style.color = 'var(--success-color)';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -289,6 +412,9 @@
|
||||
if (data.tasks !== undefined) {
|
||||
statTasks.textContent = data.tasks;
|
||||
}
|
||||
if (data.version) {
|
||||
document.getElementById('hub-version').textContent = 'v' + data.version;
|
||||
}
|
||||
} else if (data.type === "node_joined") {
|
||||
chatBox.classList.remove('hidden');
|
||||
const msgDiv = document.createElement('div');
|
||||
@@ -321,6 +447,12 @@
|
||||
const nodeId = data.node_id || "?";
|
||||
const ms = data.duration_ms || 0;
|
||||
|
||||
// Päivitetään metriikat
|
||||
metrics.tasks++;
|
||||
metrics.totalTokens += (en.token_count || 0) + (fi.token_count || 0);
|
||||
metrics.totalTimeMs += ms;
|
||||
updateMetrics();
|
||||
|
||||
const enCpt = parseFloat((en.chars_per_token || 0).toFixed(2));
|
||||
const fiCpt = parseFloat((fi.chars_per_token || 0).toFixed(2));
|
||||
|
||||
@@ -432,6 +564,52 @@
|
||||
`Varaus: <span>${deviceInfo.allocated_gb} GB</span>`
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
// Yhteensopivuusbanneri
|
||||
const banner = document.getElementById('compat-banner');
|
||||
banner.style.display = 'block';
|
||||
|
||||
if (hasWebGPU) {
|
||||
banner.className = 'compat-banner gpu';
|
||||
banner.innerHTML = `GPU-kiihdytys aktiivinen — ${gpuStr}`;
|
||||
} else {
|
||||
// Tunnistetaan selain ohjeen personointia varten
|
||||
const ua = navigator.userAgent;
|
||||
const isFirefox = ua.includes('Firefox');
|
||||
const isChrome = ua.includes('Chrome') && !ua.includes('Edg');
|
||||
const isBrave = ua.includes('Brave') || (navigator.brave && navigator.brave.isBrave);
|
||||
const isSafari = ua.includes('Safari') && !ua.includes('Chrome');
|
||||
const isLinux = ua.includes('Linux');
|
||||
|
||||
let browserTip = '';
|
||||
if (isFirefox) {
|
||||
browserTip = `
|
||||
<p><strong>Firefox</strong> ei tue WebGPU:ta oletuksena.</p>
|
||||
<p>Ota käyttöön: <code>about:config</code> → <code>dom.webgpu.enabled</code> = <code>true</code> → käynnistä uudelleen.</p>
|
||||
<p>Tai vaihda Chromeen/Braveen — niissä WebGPU toimii oletuksena.</p>`;
|
||||
} else if ((isChrome || isBrave) && isLinux) {
|
||||
const browser = isBrave ? 'brave-browser' : 'google-chrome';
|
||||
browserTip = `
|
||||
<p><strong>${isBrave ? 'Brave' : 'Chrome'} + Linux</strong>: GPU-ajuri ei ehkä tarjoa WebGPU:ta Wayland-ympäristössä.</p>
|
||||
<p>Kokeile käynnistää selain komentoriviltä:</p>
|
||||
<code>${browser} --enable-unsafe-webgpu --enable-features=Vulkan --ignore-gpu-blocklist --use-angle=vulkan --ozone-platform=x11</code>`;
|
||||
} else if (isSafari) {
|
||||
browserTip = `
|
||||
<p><strong>Safari</strong>: WebGPU on tuettu versiosta 26 alkaen (macOS Tahoe).</p>
|
||||
<p>Vanhemmissa versioissa: Develop → Feature Flags → WebGPU.</p>`;
|
||||
} else {
|
||||
browserTip = `
|
||||
<p>Selaimesi ei tue WebGPU:ta. Kokeile <strong>Chrome 113+</strong> tai <strong>Brave</strong>.</p>`;
|
||||
}
|
||||
|
||||
banner.className = 'compat-banner cpu';
|
||||
banner.innerHTML = `
|
||||
<details>
|
||||
<summary>CPU-laskenta (WebGPU ei käytettävissä) — klikkaa ohjeita</summary>
|
||||
${browserTip}
|
||||
<p style="margin-top:8px;color:#8b949e;font-size:12px">Laskenta toimii silti CPU:lla, mutta GPU-kiihdytys olisi nopeampi.</p>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
document.getElementById('initial-state').classList.add('hidden');
|
||||
document.getElementById('active-state').classList.remove('hidden');
|
||||
btn.style.display = 'none';
|
||||
@@ -440,6 +618,7 @@
|
||||
console.log("Ladataan Burn Wasm -binääriä...");
|
||||
await init();
|
||||
window.wasm_active = true;
|
||||
metrics.startTime = Date.now();
|
||||
|
||||
// Varmistetaan, että Wasm saa nykyisen sliderin arvon heti kärkeen
|
||||
set_gpu_load(parseInt(loadSlider.value));
|
||||
|
||||
Reference in New Issue
Block a user