diff --git a/network-poc/README.md b/network-poc/README.md
index eb6a23c..e488289 100644
--- a/network-poc/README.md
+++ b/network-poc/README.md
@@ -41,14 +41,16 @@ Hajautettu AI-laskentaverkko selaimessa ja natiivina. Käyttäjät tarjoavat GPU
- `lib.rs` — Wasm-entrypoint, tehtävävalinta (`SELECTED_TASK`), WebSocket-handler, GPU/CPU-valinta
- `storage.rs` — IndexedDB read/write (tokenizer, mallin painot)
+- `sampling.rs` — Top-k sampling EOS-penaltilla (kiertää Candlen softmax Wasm-bugin)
- `smollm.rs` — SmolLM 135M Candle-inferenssi (Llama-arkkitehtuuri)
- `qwen.rs` — Qwen2.5 0.5B Candle-inferenssi (Qwen2-arkkitehtuuri)
+- `qwen_coder.rs` — Qwen2.5-Coder 0.5B/3B koodigenerointi (sama arkkitehtuuri, koodikoulutettu)
- `phi3.rs` — Phi-3 placeholder (liian iso selaimelle)
### Native Node (`native-node/src/`)
- `main.rs` — GPU-tunnistus (wgpu + NVML + sysfs + Apple), HF Hub -lataus, WS-yhteys
-- `inference.rs` — Qwen2.5-0.5B Candle-inferenssi, KV-cache reset per prompti, mmap-lataus
+- `inference.rs` — Qwen2.5-0.5B Candle-inferenssi, CUDA/CPU, KV-cache reset per prompti, mmap-lataus
## Kehitysympäristö
@@ -87,6 +89,7 @@ Solmu → hub:
| `llm_done` | LLM-tulos: `{response, tokens_generated, tokens_per_sec}` |
| `llm_chunk` | Streaming-token |
| `download_progress` | Mallin latauksen edistyminen |
+| `user_text` | Käyttäjän oma teksti: `{text, task_type}` |
## API-endpointit
@@ -105,6 +108,7 @@ Solmu → hub:
- **IP-rajoitus** — max 4 WS-yhteyttä per IP, X-Forwarded-For -tuki
- **Viestivalidointi** — pakollinen `type`, sallitut tyypit, kenttäkohtaiset rajat
- **Viestikoko** — max 16 KB per WebSocket-viesti
+- **Admin Basic Auth** — `/admin` ja `/api/*` salasanan takana (`ADMIN_PASSWORD` env, oletus: `kipina`)
- **Caddy** — automaattinen TLS (Let's Encrypt)
## Tuotanto-deploy
@@ -119,10 +123,11 @@ docker compose -f docker-compose.prod.yml down && docker compose -f docker-compo
## Tiedossa olevat rajoitukset
-- LLM-inferenssi on **greedy** (argmax) — ei temperature/top-p samplingia Wasmissa (Candlen `SoftmaxLastDim` bugi)
+- LLM-inferenssi käyttää **top-k samplingia** (k=10, EOS-penaltti) — ei täyttä temperature/top-p -tukea Wasmissa
- Qwen selaimessa: ~0.4 tok/s CPU — käyttökelpoinen demona mutta ei tuotantoon
+- Native node + CUDA: ~50-100 tok/s (RTX 4090)
- Hub broadcastaa kaikki viestit kaikille — ei kohdennettu reititystä
-- CUDA-tuki vaatii `nvidia-cuda-toolkit` asennuksen + Cargo.toml featuren
+- 3B Coder-malli vaatii ~12 GB RAM selaimessa (Wasm)
## Lisenssi
diff --git a/network-poc/USER-README.md b/network-poc/USER-README.md
index 39323ce..a139de7 100644
--- a/network-poc/USER-README.md
+++ b/network-poc/USER-README.md
@@ -15,20 +15,29 @@ Kipinä Agentic Network on hajautettu tekoälylaskentaverkko, jossa selaimet ja
jos WebGPU ei tuettu
```
-**Hub** jakaa tokenisointitehtäviä satunnaisesti 10 sekunnin välein. Solmut tokenisoivat syötteen Qwen2.5-Coder-tokenizerin avulla ja palauttavat tuloksen. Hub näyttää tulokset terminaalissa ja välittää ne dashboardiin.
+**Hub** jakaa tehtäviä (tokenisointiparit, LLM-promptit, kooditehtävät) 10 sekunnin välein. Solmut käsittelevät vain valitsemansa tehtävätyypin mukaisia viestejä.
-## Kaksi tapaa osallistua verkkoon
+## Kolme tapaa osallistua verkkoon
-### 1. Selainsolmu (Wasm + WebGPU)
-- Avaa `http://localhost:3000` | `https://kipina.studio` selaimessa ja klikkaa "Liity laskentaverkkoon"
-- Selain tunnistaa automaattisesti WebGPU-tuen — jos ei löydy, käytetään CPU-fallbackia
-- Tokenizer ladataan HuggingFacesta ensimmäisellä kerralla ja tallennetaan IndexedDB:hen
-- GPU-kuormitusta voi säätää sliderilla (0–75 %)
+### 1. Selainsolmu — Laskentaverkko
+- Avaa `http://localhost:3000` | `https://kipina.studio` ja valitse tehtävä:
+ - **Tokenisointivertailu** — EN/FI-kieliparien BPE-tokenisointitehokkuus (~7 MB lataus)
+ - **SmolLM 135M** — kevyt LLM-inferenssi (~269 MB, ~1.2 tok/s)
+ - **Qwen2.5 0.5B** — tehokkaampi LLM (~990 MB, ~0.4 tok/s)
+ - **Phi-3 Mini 3.8B** — vain native-nodella
+- WebGPU tunnistetaan automaattisesti, CPU-fallback jos ei tuettu
+- Mallit ja tokenizerit cachetetaan IndexedDB:hen
-### 2. Natiivi-node (Rust + NVML)
+### 2. Selainsolmu — Koodilaboratorio
+- Erillinen välilehti: **Qwen2.5-Coder** koodigenerointi
+- Valittavissa **0.5B** (nopea) tai **3B** (laadukas, 6.2 GB lataus)
+- Oma promptti: kirjoita Python-ohjelmointitehtävä ja paina "Generate"
+- Syntaksikorostettu koodivastaus
+
+### 3. Natiivi-node (Rust + CUDA/CPU)
+- Qwen2.5-0.5B-Instruct inferenssi CUDA:lla (~50-100 tok/s RTX 4090) tai CPU:lla (~11 tok/s)
- Kerää nvidia-smi-tason laitteistotiedot: GPU-nimi, VRAM, lämpötila, kuormitus
-- Raportoi järjestelmätiedot: CPU-malli, ytimet, RAM, OS
-- Yhdistää hubiin ja vastaanottaa tehtäviä
+- Lataa mallin automaattisesti HuggingFace Hubista (~990 MB, cachetetaan)
## Käynnistys
@@ -65,23 +74,26 @@ CARGO_TARGET_DIR=target-native HUB_URL=ws://localhost:3000/ws ALLOCATED_GB=4 car
CARGO_TARGET_DIR=target-native HUB_URL=wss://kipina.studio/ws ALLOCATED_GB=4 cargo run --release -p native-node
```
-### CUDA-tuki (valinnainen)
+### CUDA-tuki
-Jos koneessa on NVIDIA GPU ja CUDA toolkit:
+CUDA on oletuksena päällä native-nodessa. Vaatii `nvidia-cuda-toolkit`:n:
```bash
-# Asenna CUDA toolkit (Ubuntu/Pop!_OS)
+# Asenna (Ubuntu/Pop!_OS)
sudo apt install nvidia-cuda-toolkit
-# Muokkaa native-node/Cargo.toml:
-# candle-core = { version = "0.8", features = ["cuda"] }
+# Tarkista
+nvcc --version
-# Aja — malli käyttää automaattisesti GPU:ta
+# Aja — tunnistaa CUDA:n automaattisesti, fallback CPU:lle
CARGO_TARGET_DIR=target-native HUB_URL=ws://localhost:3000/ws cargo run --release -p native-node
-CARGO_TARGET_DIR=target-native HUB_URL=ws://kipina.studio/ws cargo run --release -p native-node
+# Tuotantoon
+CARGO_TARGET_DIR=target-native HUB_URL=wss://kipina.studio/ws cargo run --release -p native-node
```
+Jos CUDA:a ei ole, poista feature: `candle-core = { version = "0.8" }` (ilman `features = ["cuda"]`).
+
## WebGPU-asetukset selaimessa
WebGPU ei ole oletuksena päällä kaikissa selaimissa. Jos "Liity laskentaverkkoon" -nappi käynnistää CPU-fallbackin vaikka koneessa on näytönohjain:
@@ -114,19 +126,31 @@ flatpak run com.brave.Browser --enable-unsafe-webgpu --enable-features=Vulkan --
```
network-poc/
├── hub/ # Keskuspalvelin (Rust + Axum)
-│ └── src/main.rs # WebSocket-reititin, tehtävien jakelu, statistiikat
+│ └── src/
+│ ├── main.rs # WebSocket-reititin, tehtävien jakelu, admin HTML, Basic Auth
+│ └── db.rs # SQLite: node_sessions, pair_results
├── node/ # Selainsolmu (Rust → Wasm)
│ └── src/
-│ ├── lib.rs # WebGPU/NdArray-laskenta, tokenisaatio, WS-yhteys
-│ └── storage.rs # IndexedDB-välimuisti (tokenizer)
-├── native-node/ # Natiivi-solmu (Rust)
-│ └── src/main.rs # NVML GPU-tunnistus, sysinfo, WS-yhteys
+│ ├── lib.rs # Wasm-entrypoint, tehtävävalinta, WS-handler
+│ ├── storage.rs # IndexedDB-välimuisti
+│ ├── sampling.rs # Top-k sampling (EOS-penaltti)
+│ ├── smollm.rs # SmolLM 135M inferenssi
+│ ├── qwen.rs # Qwen2.5 0.5B inferenssi
+│ ├── qwen_coder.rs # Qwen2.5-Coder 0.5B/3B koodigenerointi
+│ └── phi3.rs # Phi-3 placeholder
+├── native-node/ # Natiivi-solmu (Rust + CUDA)
+│ └── src/
+│ ├── main.rs # GPU-tunnistus, WS-yhteys, tehtäväkäsittely
+│ └── inference.rs # Qwen2.5-0.5B Candle-inferenssi (CUDA/CPU)
├── static/
-│ ├── index.html # Dashboard-käyttöliittymä
+│ ├── index.html # Dashboard + Koodilaboratorio
│ └── pkg/ # Wasm-build (generoidaan)
-├── docker-compose.yml
-├── Dockerfile.dev # Hub + Wasm-build
-└── Dockerfile.native-node
+├── deploy.sh # Lokaali build → palvelimelle
+├── docker-compose.yml # Kehitys
+├── docker-compose.prod.yml # Tuotanto (Caddy + Hub)
+├── docker-compose.client.yml # Client-nodejen Docker
+├── Dockerfile.prod # Tuotanto-image (cache mount)
+└── Caddyfile.prod # TLS + reverse proxy
```
## Ympäristömuuttujat
@@ -135,15 +159,27 @@ network-poc/
|---|---|---|
| `HUB_URL` | `ws://hub:3000/ws` | Hub-palvelimen WebSocket-osoite (native-node) |
| `ALLOCATED_GB` | `4` | Solmun varaama muisti verkosta (GB) |
+| `ADMIN_PASSWORD` | `kipina` | Admin-sivun ja API:n salasana (Basic Auth) |
+| `DATABASE_PATH` | `nodes.db` | SQLite-tietokannan polku |
+| `STATIC_DIR` | `../static` | Staattisten tiedostojen kansio |
-## Kehitysvaihe
+## Admin-sivu
-Tämä on proof-of-concept. Toimivat osat:
-- Hub-palvelin, WebSocket-viestintä, dashboard
-- WebGPU-tensorilaskenta selaimessa (Burn + Wgpu)
-- CPU-fallback selaimissa ilman WebGPU-tukea (Burn + NdArray)
-- Natiivi-node nvidia-smi-tason laitteistotiedoilla
-- Qwen2.5-Coder-tokenizer + IndexedDB-välimuisti
-- GPU-kuormituksen säätö (duty cycle throttling)
+`https://kipina.studio/admin` (Basic Auth, salasana: `ADMIN_PASSWORD`)
-Seuraavaksi: oikea LLM-inferenssi hajautetusti (mallin painojen lataus, transformer-arkkitehtuuri Wasm/WebGPU:lla).
+Sisältää:
+- Node-sessiot: IP, laitetiedot, GPU, WebGPU-tuki, tehtävätyyppi, uptime
+- Tokenisointitulokset: EN/FI-vertailut, ylikustannus-%
+- Yhteenvetotilastot: sessiot, WebGPU vs CPU, keskiarvot
+
+## Projektin tila
+
+Toimivat ominaisuudet:
+- Tokenisointivertailu (EN/FI, BPE, top-k sampling)
+- SmolLM 135M inferenssi selaimessa (Candle + Wasm)
+- Qwen2.5 0.5B inferenssi selaimessa (Candle + Wasm)
+- Qwen2.5-Coder 0.5B/3B koodigenerointi (Koodilaboratorio-välilehti)
+- Native node + CUDA (RTX 4090: ~50-100 tok/s)
+- Admin-dashboard + SQLite + Basic Auth
+- Deploy-skripti (lokaali build → palvelin)
+- WebGPU + CPU fallback, GPU-tunnistus (NVIDIA/AMD/Apple)
diff --git a/network-poc/hub/src/main.rs b/network-poc/hub/src/main.rs
index f4dd885..f20fa21 100644
--- a/network-poc/hub/src/main.rs
+++ b/network-poc/hub/src/main.rs
@@ -157,13 +157,30 @@ async function load() {
{v: stats.avg_overhead_pct + '%', l: 'FI ylikust. (ka.)'},
].map(s => `
${s.v}
${s.l}
`).join('');
- // Sessions
+ // Sessions — lajittelu: 1) aktiiviset nodet (online + ei viewer), 2) katsojat (online + viewer), 3) offline
+ const taskNames = {'tokenize':'Tokenisaatio','smollm-135m':'SmolLM 135M','qwen-05b':'Qwen2.5 0.5B','phi3-mini':'Phi-3 Mini','qwen-coder-05b':'Coder 0.5B','qwen-coder-3b':'Coder 3B','viewer':'Katsoja'};
+ sessions.sort((a, b) => {
+ const aOnline = !a.disconnected_at;
+ const bOnline = !b.disconnected_at;
+ const aViewer = a.selected_task === 'viewer';
+ const bViewer = b.selected_task === 'viewer';
+ // Online ennen offlinea
+ if (aOnline !== bOnline) return aOnline ? -1 : 1;
+ // Online: aktiiviset nodet ennen katsojia
+ if (aOnline && bOnline && aViewer !== bViewer) return aViewer ? 1 : -1;
+ // Saman ryhmän sisällä: uusin ensin
+ return new Date(b.connected_at) - new Date(a.connected_at);
+ });
+
document.getElementById('sessions-body').innerHTML = sessions.map(s => {
const online = !s.disconnected_at;
- const status = online ? 'ONLINE' : 'offline';
+ const isViewer = s.selected_task === 'viewer';
+ const status = online
+ ? (isViewer ? 'CONNECTED' : 'ACTIVE')
+ : 'offline';
const typeBadge = s.node_type === 'native' ? badge('native','blue') : badge('browser','yellow');
- const taskNames = {'tokenize':'Tokenisaatio','smollm-135m':'SmolLM 135M','qwen-05b':'Qwen2.5 0.5B','phi3-mini':'Phi-3 Mini'};
- const taskBadge = badge(taskNames[s.selected_task] || s.selected_task || 'tokenize', s.selected_task === 'tokenize' ? 'green' : 'blue');
+ const taskColor = isViewer ? 'yellow' : s.selected_task === 'tokenize' ? 'green' : 'blue';
+ const taskBadge = badge(taskNames[s.selected_task] || s.selected_task || '?', taskColor);
const gpuBadge = s.has_webgpu ? badge('WebGPU','green') : badge('CPU','red');
const gpu = s.gpu_name ? `${s.gpu_name}` : '-';
const vram = s.vram_total_mb ? `${s.vram_total_mb} MB` : '-';
@@ -346,27 +363,73 @@ async fn main() {
}
async fn api_sessions(
+ headers: axum::http::HeaderMap,
axum::extract::State(state): axum::extract::State>,
-) -> impl IntoResponse {
- axum::Json(state.db.get_sessions(200))
+) -> axum::response::Response {
+ if !check_admin_auth(&headers) { return admin_unauthorized(); }
+ axum::Json(state.db.get_sessions(200)).into_response()
}
async fn api_pairs(
+ headers: axum::http::HeaderMap,
axum::extract::State(state): axum::extract::State>,
-) -> impl IntoResponse {
- axum::Json(state.db.get_pair_results(500))
+) -> axum::response::Response {
+ if !check_admin_auth(&headers) { return admin_unauthorized(); }
+ axum::Json(state.db.get_pair_results(500)).into_response()
}
async fn api_stats(
+ headers: axum::http::HeaderMap,
axum::extract::State(state): axum::extract::State>,
-) -> impl IntoResponse {
+) -> axum::response::Response {
+ if !check_admin_auth(&headers) { return admin_unauthorized(); }
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)
+ axum::Json(stats).into_response()
}
-async fn admin_page() -> impl IntoResponse {
- axum::response::Html(ADMIN_HTML)
+fn check_admin_auth(headers: &axum::http::HeaderMap) -> bool {
+ let password = std::env::var("ADMIN_PASSWORD").unwrap_or_else(|_| "kipina".to_string());
+ if let Some(auth) = headers.get("authorization").and_then(|v| v.to_str().ok()) {
+ if auth.starts_with("Basic ") {
+ if let Ok(decoded) = String::from_utf8(
+ base64_decode(auth.trim_start_matches("Basic ").trim())
+ ) {
+ // Tarkistetaan "user:password" — käyttäjänimi ei väliä
+ if let Some(pass) = decoded.split(':').nth(1) {
+ return pass == password;
+ }
+ }
+ }
+ }
+ false
+}
+
+fn base64_decode(input: &str) -> Vec {
+ // Yksinkertainen base64-dekooderi
+ const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ let mut out = Vec::new();
+ let bytes: Vec = input.bytes().filter(|&b| b != b'=').collect();
+ for chunk in bytes.chunks(4) {
+ let vals: Vec = chunk.iter().filter_map(|&b| TABLE.iter().position(|&t| t == b).map(|p| p as u8)).collect();
+ if vals.len() >= 2 { out.push((vals[0] << 2) | (vals[1] >> 4)); }
+ if vals.len() >= 3 { out.push((vals[1] << 4) | (vals[2] >> 2)); }
+ if vals.len() >= 4 { out.push((vals[2] << 6) | vals[3]); }
+ }
+ out
+}
+
+fn admin_unauthorized() -> axum::response::Response {
+ axum::response::Response::builder()
+ .status(401)
+ .header("WWW-Authenticate", "Basic realm=\"Kipinä Admin\"")
+ .body(axum::body::Body::from("Unauthorized"))
+ .unwrap()
+}
+
+async fn admin_page(headers: axum::http::HeaderMap) -> axum::response::Response {
+ if !check_admin_auth(&headers) { return admin_unauthorized(); }
+ axum::response::Html(ADMIN_HTML).into_response()
}
async fn ws_handler(
diff --git a/network-poc/native-node/Cargo.toml b/network-poc/native-node/Cargo.toml
index 36344e6..90896b0 100644
--- a/network-poc/native-node/Cargo.toml
+++ b/network-poc/native-node/Cargo.toml
@@ -12,7 +12,7 @@ serde_json = "1.0"
sysinfo = "0.30"
nvml-wrapper = "0.10"
wgpu = "24"
-candle-core = { version = "0.8" }
+candle-core = { version = "0.8", features = ["cuda"] }
candle-nn = "0.8"
candle-transformers = "0.8"
hf-hub = "0.4"
diff --git a/network-poc/node/src/lib.rs b/network-poc/node/src/lib.rs
index ef07ece..03df2db 100644
--- a/network-poc/node/src/lib.rs
+++ b/network-poc/node/src/lib.rs
@@ -7,6 +7,7 @@ use burn::tensor::Tensor;
use burn::backend::{Wgpu, NdArray};
pub mod storage;
+pub mod sampling;
pub mod smollm;
pub mod qwen;
pub mod qwen_coder;
diff --git a/network-poc/node/src/qwen.rs b/network-poc/node/src/qwen.rs
index 1494e3c..42be9ec 100644
--- a/network-poc/node/src/qwen.rs
+++ b/network-poc/node/src/qwen.rs
@@ -154,7 +154,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc>) {
} else {
logits // jo [vocab_size]
};
- let mut next_token = logits.argmax(0).unwrap().to_vec0::().unwrap();
+ let mut next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
console_log!("[Qwen] Ensimmäinen token: {}", next_token);
let eos_token = 151645u32; // <|endoftext|> for Qwen2.5
@@ -188,7 +188,7 @@ pub async fn run_qwen_inference(prompt: String, ws: Rc>) {
} else {
logits
};
- next_token = logits.argmax(0).unwrap().to_vec0::().unwrap();
+ next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
pos += 1;
if next_token == eos_token { break; }
diff --git a/network-poc/node/src/qwen_coder.rs b/network-poc/node/src/qwen_coder.rs
index d01ff01..d22d40c 100644
--- a/network-poc/node/src/qwen_coder.rs
+++ b/network-poc/node/src/qwen_coder.rs
@@ -173,8 +173,22 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use
let load_time = perf.now() - start_load;
console_log!("[Coder] Malli ladattu ({:.0}ms). Generoidaan...", load_time);
- // Muotoillaan chat-template
- let formatted = format!("<|im_start|>system\nYou are a Python coding assistant. Write only code, no explanations.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", prompt);
+ // Parsitaan JSON-prompti tai käytetään teksti sellaisenaan
+ let (actual_prompt, system_msg, max_new_tokens) = if prompt.starts_with('{') {
+ if let Ok(json) = serde_json::from_str::(&prompt) {
+ let p = json.get("prompt").and_then(|v| v.as_str()).unwrap_or(&prompt).to_string();
+ let s = json.get("system").and_then(|v| v.as_str())
+ .unwrap_or("You are a Python coding assistant. Write only code, no explanations.").to_string();
+ let m = json.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(128) as usize;
+ (p, s, m)
+ } else {
+ (prompt.clone(), "You are a Python coding assistant. Write only code, no explanations.".to_string(), 128)
+ }
+ } else {
+ (prompt.clone(), "You are a Python coding assistant. Write only code, no explanations.".to_string(), 128)
+ };
+
+ let formatted = format!("<|im_start|>system\n{}<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n", system_msg, actual_prompt);
let encoding = match tokenizer.encode(formatted.as_str(), true) {
Ok(e) => e,
@@ -185,7 +199,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use
console_log!("[Coder] Syöte: {} tokenia", input_len);
let start_gen = perf.now();
- let max_new_tokens = 128; // Koodille enemmän tokeneita
+ // max_new_tokens tulee JSON-promptista tai oletuksena 128
let mut generated_text = String::new();
let mut tokens_generated: usize = 0;
let eos_token = 151645u32;
@@ -206,7 +220,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use
} else {
logits
};
- let mut next_token = logits.argmax(0).unwrap().to_vec0::().unwrap();
+ let mut next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
if next_token != eos_token {
if let Ok(text) = tokenizer.decode(&[next_token], true) {
@@ -237,7 +251,7 @@ pub async fn run_coder_inference(prompt: String, ws: Rc>, use
} else {
logits
};
- next_token = logits.argmax(0).unwrap().to_vec0::().unwrap();
+ next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
pos += 1;
if next_token == eos_token { break; }
diff --git a/network-poc/node/src/sampling.rs b/network-poc/node/src/sampling.rs
new file mode 100644
index 0000000..e0a39f5
--- /dev/null
+++ b/network-poc/node/src/sampling.rs
@@ -0,0 +1,47 @@
+use candle_core::Tensor;
+
+/// Top-k sampling ilman softmaxia — kiertää Candlen SoftmaxLastDim Wasm-bugin.
+/// Valitsee top-k logiteista ja poimii satunnaisen (painotettu).
+/// Jos k=1, toimii kuten argmax (greedy).
+pub fn sample_top_k(logits: &Tensor, k: usize, eos_penalty: f32) -> u32 {
+ // Muunnetaan Vec:ksi
+ let logits_vec: Vec = logits.to_vec1::().unwrap_or_default();
+ if logits_vec.is_empty() { return 0; }
+
+ // Rangotaan ja otetaan top-k indeksit
+ let mut indexed: Vec<(usize, f32)> = logits_vec.iter().enumerate().map(|(i, &v)| (i, v)).collect();
+ indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
+ indexed.truncate(k);
+
+ // EOS-penaltti: vähennetään EOS-tokenin logitia
+ for item in indexed.iter_mut() {
+ if item.0 == 2 || item.0 == 151645 { // SmolLM EOS=2, Qwen EOS=151645
+ item.1 -= eos_penalty;
+ }
+ }
+
+ if k == 1 {
+ return indexed[0].0 as u32;
+ }
+
+ // Yksinkertainen "softmax" top-k:lle CPU:lla
+ let max_logit = indexed.iter().map(|x| x.1).fold(f32::NEG_INFINITY, f32::max);
+ let exps: Vec = indexed.iter().map(|x| (x.1 - max_logit).exp()).collect();
+ let sum: f32 = exps.iter().sum();
+ let probs: Vec = exps.iter().map(|e| e / sum).collect();
+
+ // Satunnainen valinta kumulatiivisella todennäköisyydellä
+ // Käytetään yksinkertaista XorShift-satunnaislukugeneraattoria (ei tarvita getrandom)
+ let seed = (js_sys::Date::now() * 1000.0) as u64;
+ let rand_val = ((seed ^ (seed >> 13) ^ (seed << 7)) % 10000) as f32 / 10000.0;
+
+ let mut cumulative = 0.0;
+ for (i, p) in probs.iter().enumerate() {
+ cumulative += p;
+ if rand_val < cumulative {
+ return indexed[i].0 as u32;
+ }
+ }
+
+ indexed[0].0 as u32
+}
diff --git a/network-poc/node/src/smollm.rs b/network-poc/node/src/smollm.rs
index afd2d8e..2176467 100644
--- a/network-poc/node/src/smollm.rs
+++ b/network-poc/node/src/smollm.rs
@@ -196,7 +196,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc>) {
} else {
logits
};
- let mut next_token = logits.argmax(0).unwrap().to_vec0::().unwrap();
+ let mut next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
console_log!("[SmolLM] Ensimmäinen generoitu token: {}", next_token);
pos = input_len;
@@ -229,7 +229,7 @@ pub async fn run_smollm_inference(prompt: String, ws: Rc>) {
} else {
logits
};
- next_token = logits.argmax(0).unwrap().to_vec0::().unwrap();
+ next_token = crate::sampling::sample_top_k(&logits, 10, 5.0);
pos += 1;
if next_token == 2 { break; }
diff --git a/network-poc/static/index.html b/network-poc/static/index.html
index 2ca495e..4cac502 100644
--- a/network-poc/static/index.html
+++ b/network-poc/static/index.html
@@ -567,9 +567,37 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+ JSON syntax
+
+{
+ "prompt": "Write a bubble sort",
+ "system": "You are a Python expert. Write only code.",
+ "max_tokens": 128,
+ "language": "python"
+}
+
+ Fields:
+ prompt (required) — the coding task
+ system — system prompt override
+ max_tokens — max tokens to generate (default: 128)
+ language — hint for syntax highlighting
+