diff --git a/network-poc/frontend/public/download/kipina-node-linux-x86_64 b/network-poc/frontend/public/download/kipina-node-linux-x86_64 index 915621a..fc72542 100755 Binary files a/network-poc/frontend/public/download/kipina-node-linux-x86_64 and b/network-poc/frontend/public/download/kipina-node-linux-x86_64 differ diff --git a/network-poc/native-node/src/inference.rs b/network-poc/native-node/src/inference.rs index 0bbcdb5..ba057da 100644 --- a/network-poc/native-node/src/inference.rs +++ b/network-poc/native-node/src/inference.rs @@ -69,6 +69,10 @@ impl LlmEngine { self.model.borrow().clone() } + pub fn ollama_url(&self) -> &str { + &self.ollama_url + } + pub fn set_model(&self, new_model: String) { *self.model.borrow_mut() = new_model; } @@ -91,6 +95,32 @@ impl LlmEngine { } } + /// Hakee käynnissä olevan mallin VRAM-tilan (ollama ps) + pub async fn fetch_ps(&self) -> Result, String> { + let resp = self.client.get(format!("{}/api/ps", self.ollama_url)) + .send() + .await + .map_err(|e| format!("Ollama ps: {}", e))?; + + if !resp.status().is_success() { + return Err(format!("Ollama ps HTTP {}", resp.status())); + } + + let body: serde_json::Value = resp.json().await + .map_err(|e| format!("Ollama ps json: {}", e))?; + + let models = body["models"].as_array(); + if let Some(arr) = models { + if let Some(m) = arr.first() { + let name = m["name"].as_str().unwrap_or("?").to_string(); + let size = m["size"].as_u64().unwrap_or(0); + let size_vram = m["size_vram"].as_u64().unwrap_or(0); + return Ok(Some(ModelVramStatus { name, size, size_vram })); + } + } + Ok(None) // ei ladattua mallia + } + /// Hakee kaikki Ollamaan asennetut mallit pub async fn fetch_models(&self) -> Result { let resp = self.client.get(format!("{}/api/tags", self.ollama_url)) @@ -184,3 +214,32 @@ pub struct GenerateResult { pub duration_ms: f64, pub tokens_per_sec: f64, } + +pub struct ModelVramStatus { + pub name: String, + pub size: u64, // kokonaiskoko (tavuina) + pub size_vram: u64, // VRAM:ssa oleva osuus (tavuina) +} + +impl ModelVramStatus { + pub fn fully_in_vram(&self) -> bool { + self.size > 0 && self.size_vram >= self.size + } + + pub fn vram_percent(&self) -> f64 { + if self.size == 0 { return 0.0; } + (self.size_vram as f64 / self.size as f64) * 100.0 + } + + pub fn display(&self) -> String { + let size_gb = self.size as f64 / 1_073_741_824.0; + let vram_gb = self.size_vram as f64 / 1_073_741_824.0; + if self.fully_in_vram() { + format!("✓ {} ({:.1} GB) — 100% GPU", self.name, size_gb) + } else if self.size_vram == 0 { + format!("✗ {} ({:.1} GB) — 100% CPU", self.name, size_gb) + } else { + format!("◐ {} ({:.1}/{:.1} GB VRAM, {:.0}% GPU)", self.name, vram_gb, size_gb, self.vram_percent()) + } + } +} diff --git a/network-poc/native-node/src/main.rs b/network-poc/native-node/src/main.rs index 3a8cee9..472b552 100644 --- a/network-poc/native-node/src/main.rs +++ b/network-poc/native-node/src/main.rs @@ -363,6 +363,38 @@ async fn main() { st.push_log("System", format!("Malli valmis: {}", active_model), None); } + // Haetaan VRAM-tila heti ja käynnistetään taustapäivitys (30s välein) + if let Some(ref engine) = llm { + if let Ok(Some(ps)) = engine.fetch_ps().await { + let mut st = tui_state.write().await; + st.vram_status = ps.display(); + st.push_log("System", format!("VRAM: {}", ps.display()), None); + } + let vram_engine_url = engine.ollama_url().to_string(); + let vram_state = tui_state.clone(); + tokio::spawn(async move { + let client = reqwest::Client::new(); + loop { + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + if let Ok(resp) = client.get(format!("{}/api/ps", vram_engine_url)).send().await { + if let Ok(body) = resp.json::().await { + if let Some(arr) = body["models"].as_array() { + if let Some(m) = arr.first() { + let name = m["name"].as_str().unwrap_or("?").to_string(); + let size = m["size"].as_u64().unwrap_or(0); + let size_vram = m["size_vram"].as_u64().unwrap_or(0); + let status = inference::ModelVramStatus { name, size, size_vram }; + vram_state.write().await.vram_status = status.display(); + } else { + vram_state.write().await.vram_status = "Ei ladattua mallia".to_string(); + } + } + } + } + } + }); + } + // Käynnistetään graafinen TUI vain jos stdin on terminaali (ei taustaprosessina) let ui_state = tui_state.clone(); if std::io::stdin().is_terminal() { diff --git a/network-poc/native-node/src/tui_dashboard.rs b/network-poc/native-node/src/tui_dashboard.rs index d763ee6..cf24eef 100644 --- a/network-poc/native-node/src/tui_dashboard.rs +++ b/network-poc/native-node/src/tui_dashboard.rs @@ -36,6 +36,8 @@ pub struct DashboardState { pub last_tokens_sec: f64, pub network_active_nodes: usize, pub network_total_tasks: u64, + // VRAM-tila (ollama ps) + pub vram_status: String, // Mallivalikko pub model_picker_open: bool, pub model_picker_items: Vec, @@ -56,6 +58,7 @@ impl DashboardState { last_tokens_sec: 0.0, network_active_nodes: 1, // oletetaan itsemme network_total_tasks: 0, + vram_status: "Haetaan...".to_string(), model_picker_open: false, model_picker_items: Vec::new(), model_picker_idx: 0, @@ -182,7 +185,7 @@ fn ui(f: &mut ratatui::Frame, st: &DashboardState) { let body_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(7), // Yläosan info ja tehtävä + Constraint::Length(8), // Yläosan info ja tehtävä Constraint::Min(0), // Lokit / Chat alas ].as_ref()) .split(chunks[1]); @@ -197,8 +200,8 @@ fn ui(f: &mut ratatui::Frame, st: &DashboardState) { // Vasen paneeli: Laitteisto, Malli & Verkosto let info_text = format!( - "🚀 Malli: {}\n💻 Järjestelmä: {}\n📊 Tehdyt: {} | Nopeus: {} t/s\n🌐 Verkosto: {} solmua | {} tehtävää", - st.model_name, st.sys_info, st.tasks_completed, st.last_tokens_sec, st.network_active_nodes, st.network_total_tasks + "🚀 Malli: {}\n🎮 VRAM: {}\n💻 Järjestelmä: {}\n📊 Tehdyt: {} | Nopeus: {} t/s\n🌐 Verkosto: {} solmua | {} tehtävää", + st.model_name, st.vram_status, st.sys_info, st.tasks_completed, st.last_tokens_sec, st.network_active_nodes, st.network_total_tasks ); let left_panel = Paragraph::new(info_text) .block(Block::default().title(" Laitteisto ja AI ").borders(Borders::ALL))