eka vedos
This commit is contained in:
31
network-poc/node/Cargo.toml
Normal file
31
network-poc/node/Cargo.toml
Normal 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
235
network-poc/node/src/lib.rs
Normal 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(())
|
||||
}
|
||||
62
network-poc/node/src/storage.rs
Normal file
62
network-poc/node/src/storage.rs
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user