eka vedos

This commit is contained in:
2026-04-01 17:54:08 +03:00
commit 46848ee027
20 changed files with 1962 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
[package]
name = "node"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.91"
js-sys = "0.3.68"
web-sys = { version = "0.3.68", features = [
"Window",
"Document",
"HtmlElement",
"WebSocket",
"MessageEvent",
"console",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
burn = { version = "0.14.0", features = ["wgpu", "ndarray"] }
burn-wgpu = "0.14.0"
burn-ndarray = "0.14.0"
wasm-bindgen-futures = "0.4"
console_error_panic_hook = "0.1.7"
reqwest = { version = "0.12", default-features = false, features = ["json"] }
tokenizers = { version = "0.19.1", default-features = false, features = ["unstable_wasm"] }
rexie = "0.6"
log = "0.4"

235
network-poc/node/src/lib.rs Normal file
View File

@@ -0,0 +1,235 @@
use wasm_bindgen::prelude::*;
use web_sys::{console, WebSocket, MessageEvent};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU32, AtomicBool, Ordering};
use burn::tensor::Tensor;
use burn::backend::{Wgpu, NdArray};
pub mod storage;
macro_rules! console_log {
($($t:tt)*) => (console::log_1(&format_args!($($t)*).to_string().into()))
}
// Globaali muuttuja GPU Load Sliderille (25-100%)
static GPU_LOAD_PERCENT: AtomicU32 = AtomicU32::new(50);
// Onko WebGPU käytettävissä — asetetaan JS-puolelta käynnistyksessä
static HAS_WEBGPU: AtomicBool = AtomicBool::new(true);
#[wasm_bindgen]
pub fn set_gpu_load(load: u32) {
GPU_LOAD_PERCENT.store(load, Ordering::SeqCst);
console_log!("[Wasm] GPU Kuormitusraja vaihdettu -> {}%", load);
}
// Asynkroninen odotus WebAssemblylle
async fn sleep_ms(ms: i32) {
let promise = js_sys::Promise::new(&mut |resolve, _| {
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
.unwrap();
});
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
}
// Geneerinen tensorilaskenta — toimii millä tahansa Burn-backendillä
fn run_matmul<B: burn::tensor::backend::Backend>(size: usize) -> String {
let device = Default::default();
let dist = burn::tensor::Distribution::Default;
let t1: Tensor<B, 2> = Tensor::random([size, size], dist, &device);
let t2: Tensor<B, 2> = Tensor::random([size, size], dist, &device);
let sum = t1.matmul(t2).sum();
format!("{:?}", sum)
}
// Päättelyfunktio — valitsee backendin automaattisesti
async fn run_ai_tensor_inference(difficulty: usize) -> String {
let load_pct = GPU_LOAD_PERCENT.load(Ordering::SeqCst);
if load_pct == 0 {
sleep_ms(2000).await;
return format!("Paused (0%). Lepäillään zZz..");
}
let active_workload_size = (difficulty as f32 * (load_pct as f32 / 100.0)) as usize;
let sleep_delay = (100 - load_pct) * 10;
if sleep_delay > 0 {
sleep_ms(sleep_delay as i32).await;
}
let use_gpu = HAS_WEBGPU.load(Ordering::SeqCst);
let (backend_name, result) = if use_gpu {
("WebGPU", run_matmul::<Wgpu>(active_workload_size))
} else {
("CPU/NdArray", run_matmul::<NdArray>(active_workload_size))
};
format!("PoC {} Matmul ({}x{}) >> {}", backend_name, active_workload_size, active_workload_size, result)
}
/// Tokenisoi yhden tekstin ja palauttaa metriikat
fn tokenize_text(tokenizer: &tokenizers::Tokenizer, text: &str) -> serde_json::Value {
let char_count = text.chars().count();
let word_count = text.split_whitespace().count();
if let Ok(encoding) = tokenizer.encode(text, true) {
let token_count = encoding.get_ids().len();
let cpt = if token_count > 0 { char_count as f32 / token_count as f32 } else { 0.0 };
let tokens: Vec<String> = encoding.get_ids().iter().filter_map(|&id| {
tokenizer.decode(&[id], true).ok()
}).collect();
serde_json::json!({
"text": text,
"char_count": char_count,
"word_count": word_count,
"token_count": token_count,
"chars_per_token": (cpt * 100.0).round() / 100.0,
"tokens": tokens,
})
} else {
serde_json::json!({
"text": text,
"char_count": char_count,
"word_count": word_count,
"token_count": word_count,
"chars_per_token": 0,
"tokens": [],
})
}
}
/// Tokenisoi en/fi-parin, vertaa tehokkuutta ja lähettää tuloksen hubille
async fn run_pair_comparison(en_text: String, fi_text: String, ws: Rc<RefCell<WebSocket>>) {
let load_pct = GPU_LOAD_PERCENT.load(Ordering::SeqCst);
if load_pct == 0 { return; }
let cached_tok = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
let Some(bytes) = cached_tok else {
console_log!("[Tokenizer] Ei vielä ladattu — ohitetaan pari");
return;
};
let Ok(tokenizer) = tokenizers::Tokenizer::from_bytes(&bytes) else {
console_log!("[Tokenizer] Parsinta epäonnistui");
return;
};
let start_time = js_sys::Date::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 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);
let en_tokens = en_result["token_count"].as_u64().unwrap_or(0);
let fi_tokens = fi_result["token_count"].as_u64().unwrap_or(0);
// Token-ylikustannus: kuinka monta % enemmän tokeneita suomi tarvitsee
let overhead_pct = if en_tokens > 0 {
((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);
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,
"tokenizer": "Qwen2.5-Coder-0.5B",
});
let _ = ws.borrow().send_with_str(&pair_done.to_string());
}
#[wasm_bindgen]
pub async fn start_agent_node(hub_url: String, has_webgpu: bool, device_info_json: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once();
HAS_WEBGPU.store(has_webgpu, Ordering::SeqCst);
let backend_name = if has_webgpu { "WebGPU" } else { "CPU (NdArray)" };
console_log!("Kipinä Agent Node käynnistyy — backend: {}", backend_name);
let device_info = device_info_json.clone();
wasm_bindgen_futures::spawn_local(async move {
console_log!("[Storage] Tarkistetaan IndexedDB Qwen2.5-Coder Tokenizeria...");
let cached_tokenizer = storage::load_from_idb("tokenizer.json").await.unwrap_or(None);
if let Some(tok_bytes) = cached_tokenizer {
console_log!("[Storage] Tokenizer löytyi välimuistista! Koko: {} tavua", tok_bytes.len());
} else {
console_log!("[Storage] Ei välimuistia. Ladataan HF:stä... Odota selaimen Network-välilehdellä.");
if let Ok(resp) = reqwest::get("https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B/resolve/main/tokenizer.json").await {
if let Ok(bytes) = resp.bytes().await {
console_log!("[Storage] Tallennetaan {}-tavuinen tiedosto IndexedDB:hen pysyvästi...", bytes.len());
let _ = storage::save_to_idb("tokenizer.json", &bytes).await;
console_log!("[Storage] Tallennettu!");
}
}
}
});
let ws = WebSocket::new(&hub_url)?;
let ws_clone = Rc::new(RefCell::new(ws));
let ws_clone_2 = ws_clone.clone();
let onmessage_callback = Closure::wrap(Box::new(move |e: MessageEvent| {
if let Ok(txt) = e.data().dyn_into::<js_sys::JsString>() {
let msg: String = txt.into();
if msg.contains("pair_task") {
if let Ok(task) = serde_json::from_str::<serde_json::Value>(&msg) {
let en = task.get("en").and_then(|v| v.as_str()).unwrap_or("").to_string();
let fi = task.get("fi").and_then(|v| v.as_str()).unwrap_or("").to_string();
if !en.is_empty() && !fi.is_empty() {
let ws_for_async = ws_clone.clone();
wasm_bindgen_futures::spawn_local(async move {
run_pair_comparison(en, fi, ws_for_async).await;
});
}
}
} else if msg.contains("ai_task") {
console_log!("Hub task vastaanotettu, ajetaan GPU:lla...");
let ws_for_async = ws_clone.clone();
let diff = if msg.contains(r#""difficulty":1024"#) { 1024 } else { 512 };
// Suoritetaan inference asynkronisesti erillisessä taaskissa välttääksemme UI-jäätymisen kokonaan
wasm_bindgen_futures::spawn_local(async move {
let result = run_ai_tensor_inference(diff).await;
let reply = format!("{{\"type\":\"result\", \"status\":\"success\", \"data\":\"{}\"}}", result);
let _ = ws_for_async.borrow().send_with_str(&reply);
});
} else if msg.contains("stats") {
// Sivuutetaan statsit täällä, UI hallitsee ne aivan itse HTML:n puolella
}
}
}) as Box<dyn FnMut(MessageEvent)>);
ws_clone_2.borrow().set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
onmessage_callback.forget();
let ws_clone_3 = ws_clone_2.clone();
let onopen_callback = Closure::wrap(Box::new(move |_| {
console_log!("Yhteys Hubiin avattu!");
// Parsitaan device_info ja lisätään auth-kenttiin
let auth_msg = if let Ok(mut info) = serde_json::from_str::<serde_json::Value>(&device_info) {
if let Some(obj) = info.as_object_mut() {
obj.insert("type".to_string(), serde_json::json!("auth"));
obj.insert("status".to_string(), serde_json::json!("agent_ready"));
}
info.to_string()
} else {
r#"{"type":"auth","status":"agent_ready","allocated_gb":4}"#.to_string()
};
let _ = ws_clone_3.borrow().send_with_str(&auth_msg);
}) as Box<dyn FnMut(JsValue)>);
ws_clone_2.borrow().set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
onopen_callback.forget();
Ok(())
}

View File

@@ -0,0 +1,62 @@
use rexie::{ObjectStore, Rexie, TransactionMode};
use js_sys::Uint8Array;
use wasm_bindgen::JsValue;
const DB_NAME: &str = "kipina_qwen_db";
const STORE_NAME: &str = "weights_store";
/// Kytketään yhteys IndexedDB:hen (tai luodaan store jos sitä ei ole)
pub async fn get_db() -> Result<Rexie, rexie::Error> {
Rexie::builder(DB_NAME)
.version(1)
.add_object_store(ObjectStore::new(STORE_NAME))
.build()
.await
}
/// Tallennetaan binääridata (esim. .safetensors lohko tai tokenizer.json string) IndexedDB-välimuistiin
pub async fn save_to_idb(key: &str, data: &[u8]) -> Result<(), String> {
let db = get_db().await.map_err(|e| format!("DB Error: {}", e))?;
let transaction = db
.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
.map_err(|e| format!("Tx Error: {}", e))?;
let store = transaction.store(STORE_NAME).map_err(|e| format!("Store Error: {}", e))?;
// Konvertoidaan Rust u8-taulukko JS Uint8Array:ksi, joka on turvallisin blob IDB:lle
let js_data = Uint8Array::from(data);
store.put(&js_data, Some(&JsValue::from_str(key)))
.await
.map_err(|e| format!("Put Error: {:?}", e))?;
transaction.done().await.map_err(|e| format!("Done Error: {}", e))?;
Ok(())
}
/// Haetaan tallennettu data IndexedDB:stä key-arvon perusteella
pub async fn load_from_idb(key: &str) -> Result<Option<Vec<u8>>, String> {
let db = get_db().await.map_err(|e| format!("DB Error: {}", e))?;
let transaction = db
.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
.map_err(|e| format!("Tx Error: {}", e))?;
let store = transaction.store(STORE_NAME).map_err(|e| format!("Store Error: {}", e))?;
let js_val_req = store.get(JsValue::from_str(key)).await.map_err(|e| format!("Get Error: {:?}", e))?;
let js_val = match js_val_req {
Some(val) => val,
None => return Ok(None),
};
if js_val.is_undefined() || js_val.is_null() {
return Ok(None);
}
// Ladataan JS muisti-blockista suoraan Rustin Veg:giksi
let arr = Uint8Array::new(&js_val);
let mut vec = vec![0; arr.length() as usize];
arr.copy_to(&mut vec);
Ok(Some(vec))
}