Native node: VRAM-tila TUI:ssa (ollama ps)

- fetch_ps(): hakee /api/ps ja palauttaa ModelVramStatus
- ModelVramStatus: size vs size_vram → 100% GPU / osittainen / CPU
- TUI: uusi "VRAM: ✓ qwen3:32b (20.1 GB) — 100% GPU" -rivi
- Taustapäivitys 30s välein
- Tuore linux-x86_64 binääri
This commit is contained in:
2026-04-13 21:06:27 +03:00
parent 5e44b63b0c
commit e09962940a
4 changed files with 97 additions and 3 deletions

View File

@@ -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<Option<ModelVramStatus>, 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<serde_json::Value, String> {
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())
}
}
}

View File

@@ -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::<serde_json::Value>().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() {

View File

@@ -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<String>,
@@ -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))