feat: add hardware telemetry dashboard and unified API client for system monitoring
This commit is contained in:
1 parent
f6d441e5d7
commit
9e67084e50
7 files changed
+353
-45
No files matched your search
@@ -784,6 +784,18 @@ body {
|
||||
}
|
||||
|
||||
/* ─── Generation Stats ────────────────────────────────────────── */
|
||||
.chat-generation-warning {
|
||||
margin-top: 4px;
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--md-shape-corner-small);
|
||||
border: 1px solid color-mix(in srgb, var(--md-sys-color-warning, #f59e0b) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--md-sys-color-warning, #f59e0b) 10%, transparent);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.35;
|
||||
max-width: min(720px, 84vw);
|
||||
}
|
||||
|
||||
.chat-generation-stats {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -120,8 +120,9 @@ function App() {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
return {
|
||||
enableThinking: true,
|
||||
...parsed
|
||||
...parsed,
|
||||
enableThinking: parsed.enableThinking === true,
|
||||
maxTokens: parsed.maxTokens || 384,
|
||||
};
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -130,10 +131,10 @@ function App() {
|
||||
temperature: 0.7,
|
||||
systemPrompt: "You are a helpful local AI assistant.",
|
||||
threads: Math.max(4, Math.min(16, (navigator.hardwareConcurrency || 4) - 2)),
|
||||
enableThinking: true,
|
||||
enableThinking: false,
|
||||
// New performance settings
|
||||
gpuLayers: -1,
|
||||
maxTokens: 1024,
|
||||
maxTokens: 384,
|
||||
topP: 0.95,
|
||||
topK: 40,
|
||||
minP: 0.05,
|
||||
@@ -147,6 +148,8 @@ function App() {
|
||||
mmap: true,
|
||||
cachePrompt: true,
|
||||
defragThold: 0.1,
|
||||
batchSize: 512,
|
||||
ubatchSize: 512,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Monitor, HardDrive, MemoryStick, Thermometer, Hash, Layers,
|
||||
ChevronRight, Box, Wand2, Lightbulb, RotateCcw, Check, Palette
|
||||
} from "lucide-react";
|
||||
import { stopServer, formatBytes, getLlmStatus } from "../services/api";
|
||||
import { stopServer, formatBytes, getLlmStatus, startLlm, stopLlm } from "../services/api";
|
||||
import { THEMES } from "../themes";
|
||||
|
||||
const ASPECT_RATIOS = [
|
||||
@@ -230,6 +230,56 @@ function Settings({
|
||||
}));
|
||||
};
|
||||
|
||||
const buildTextStartOptions = (settings) => ({
|
||||
threads: settings?.threads || specs?.cpu_cores_physical || 4,
|
||||
contextSize: settings?.contextSize || 4096,
|
||||
gpuLayers: settings?.gpuLayers ?? -1,
|
||||
enableThinking: settings?.enableThinking !== false,
|
||||
flashAttn: settings?.flashAttn,
|
||||
cacheTypeK: settings?.cacheTypeK,
|
||||
cacheTypeV: settings?.cacheTypeV,
|
||||
mlock: settings?.mlock,
|
||||
mmap: settings?.mmap,
|
||||
cachePrompt: settings?.cachePrompt,
|
||||
defragThold: settings?.defragThold,
|
||||
batchSize: settings?.batchSize,
|
||||
ubatchSize: settings?.ubatchSize,
|
||||
performanceProfile: settings?.performanceProfile,
|
||||
});
|
||||
|
||||
const handleThinkingToggle = async (enabled) => {
|
||||
const nextSettings = { ...textSettings, enableThinking: enabled };
|
||||
|
||||
let status = null;
|
||||
try {
|
||||
status = await getLlmStatus();
|
||||
} catch (_) {}
|
||||
if (!status?.ready || !status?.settings?.model) {
|
||||
setTextSettings(nextSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const reload = await showConfirm({
|
||||
title: enabled ? "Reload With DeepThink?" : "Reload Without DeepThink?",
|
||||
message: "Changing DeepThink requires reloading the text model before it affects new replies. Reload now, or skip and keep the currently loaded model as-is?",
|
||||
confirmLabel: "Reload",
|
||||
cancelLabel: "Skip",
|
||||
});
|
||||
if (!reload) return;
|
||||
|
||||
try {
|
||||
setTextSettings(nextSettings);
|
||||
await stopLlm();
|
||||
await startLlm(status.settings.model, buildTextStartOptions(nextSettings));
|
||||
} catch (err) {
|
||||
await showAlert({
|
||||
title: "Reload Failed",
|
||||
message: err.message || String(err),
|
||||
danger: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAspectRatioChange = (ratio, sizeType) => {
|
||||
if (isOpenVinoNpu && ratio !== "1:1") return;
|
||||
const isSDXL = sizeType === "sdxl" && !isSD15OrCustom;
|
||||
@@ -632,6 +682,22 @@ function Settings({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="m3-slider-group">
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">Max Response Tokens</span>
|
||||
<span className="settings-value-badge">{textSettings.maxTokens || 384}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
className="m3-slider"
|
||||
value={textSettings.maxTokens || 384}
|
||||
onChange={(e) => updateTextSetting("maxTokens", parseInt(e.target.value))}
|
||||
min="64"
|
||||
max="2048"
|
||||
step="64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="m3-slider-group">
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">Min P</span>
|
||||
@@ -787,7 +853,7 @@ function Settings({
|
||||
<div className="m3-field-group">
|
||||
<PremiumToggle
|
||||
checked={textSettings.enableThinking !== false}
|
||||
onChange={(v) => updateTextSetting("enableThinking", v)}
|
||||
onChange={handleThinkingToggle}
|
||||
label="DeepThink"
|
||||
description={supportsThinking
|
||||
? "Show model's reasoning process"
|
||||
|
||||
@@ -143,6 +143,13 @@ function TextChat({
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0
|
||||
});
|
||||
|
||||
const estimateTokens = (text) => {
|
||||
const value = String(text || "").trim();
|
||||
if (!value) return 0;
|
||||
const wordCount = value.split(/\s+/).filter(Boolean).length;
|
||||
return Math.max(wordCount, Math.ceil(value.length / 4));
|
||||
};
|
||||
|
||||
const bottomRef = useRef(null);
|
||||
const completedDownloadRef = useRef("");
|
||||
@@ -254,6 +261,53 @@ function TextChat({
|
||||
prevContextSizeRef.current = currentContextSize;
|
||||
}, [textSettings?.gpuLayers, textSettings?.threads, textSettings?.contextSize]);
|
||||
|
||||
const buildTextStartOptions = (settings) => ({
|
||||
threads: settings?.threads || specs?.cpu_cores_physical || 4,
|
||||
contextSize: settings?.contextSize || 4096,
|
||||
gpuLayers: settings?.gpuLayers ?? -1,
|
||||
enableThinking: settings?.enableThinking !== false,
|
||||
flashAttn: settings?.flashAttn,
|
||||
cacheTypeK: settings?.cacheTypeK,
|
||||
cacheTypeV: settings?.cacheTypeV,
|
||||
mlock: settings?.mlock,
|
||||
mmap: settings?.mmap,
|
||||
cachePrompt: settings?.cachePrompt,
|
||||
defragThold: settings?.defragThold,
|
||||
batchSize: settings?.batchSize,
|
||||
ubatchSize: settings?.ubatchSize,
|
||||
performanceProfile: settings?.performanceProfile,
|
||||
});
|
||||
|
||||
const handleThinkingToggle = async () => {
|
||||
const enabled = textSettings.enableThinking === false;
|
||||
const nextSettings = { ...textSettings, enableThinking: enabled };
|
||||
|
||||
if (!status.ready || !status.settings?.model) {
|
||||
setTextSettings(nextSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const reload = await showConfirm({
|
||||
title: enabled ? "Reload With DeepThink?" : "Reload Without DeepThink?",
|
||||
message: "Changing DeepThink requires reloading the text model before it affects new replies. Reload now, or skip and keep the currently loaded model as-is?",
|
||||
confirmLabel: "Reload",
|
||||
cancelLabel: "Skip",
|
||||
});
|
||||
if (!reload) return;
|
||||
|
||||
setIsBusy(true);
|
||||
try {
|
||||
setTextSettings(nextSettings);
|
||||
await stopLlm();
|
||||
const result = await startLlm(status.settings.model, buildTextStartOptions(nextSettings));
|
||||
setStatus({ ...status, ...result, ready: true, running: true, settings: result.settings });
|
||||
} catch (err) {
|
||||
showAlert({ title: "Text Model Reload Failed", message: err.message || String(err), danger: true });
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!supportsVision) {
|
||||
setAttachments((current) => current.filter((attachment) => attachment.type !== "image"));
|
||||
@@ -274,7 +328,7 @@ function TextChat({
|
||||
const text = Array.isArray(m.content)
|
||||
? m.content.map(c => c.text || "").join(" ")
|
||||
: (m.content || "");
|
||||
return sum + text.split(/\s+/).length;
|
||||
return sum + estimateTokens(text) + estimateTokens(m.reasoning || "");
|
||||
}, 0);
|
||||
setTokenUsage({
|
||||
prompt_tokens: Math.round(total * 0.7),
|
||||
@@ -386,6 +440,16 @@ function TextChat({
|
||||
contextSize: textSettings?.contextSize || 4096,
|
||||
gpuLayers: textSettings?.gpuLayers ?? -1,
|
||||
enableThinking: textSettings?.enableThinking !== false,
|
||||
flashAttn: textSettings?.flashAttn,
|
||||
cacheTypeK: textSettings?.cacheTypeK,
|
||||
cacheTypeV: textSettings?.cacheTypeV,
|
||||
mlock: textSettings?.mlock,
|
||||
mmap: textSettings?.mmap,
|
||||
cachePrompt: textSettings?.cachePrompt,
|
||||
defragThold: textSettings?.defragThold,
|
||||
batchSize: textSettings?.batchSize,
|
||||
ubatchSize: textSettings?.ubatchSize,
|
||||
performanceProfile: textSettings?.performanceProfile,
|
||||
});
|
||||
setStatus({ ...status, ...result, ready: true, running: true, settings: result.settings });
|
||||
setMessages([]);
|
||||
@@ -478,6 +542,17 @@ function TextChat({
|
||||
...(systemPrompt.trim() ? [{ role: "system", content: systemPrompt.trim() }] : []),
|
||||
...nextMessages,
|
||||
];
|
||||
const promptTokenEstimate = requestMessages.reduce((sum, message) => {
|
||||
const messageText = Array.isArray(message.content)
|
||||
? message.content.map((item) => item.text || "").join(" ")
|
||||
: (message.content || "");
|
||||
return sum + estimateTokens(messageText);
|
||||
}, 0);
|
||||
setTokenUsage({
|
||||
prompt_tokens: promptTokenEstimate,
|
||||
completion_tokens: 0,
|
||||
total_tokens: promptTokenEstimate,
|
||||
});
|
||||
|
||||
let assistantText = "";
|
||||
let assistantReasoning = "";
|
||||
@@ -487,14 +562,20 @@ function TextChat({
|
||||
let thinkingEndedAt = null;
|
||||
let thinkingDuration = 0;
|
||||
|
||||
const baseMaxTokens = textSettings?.maxTokens || 384;
|
||||
const effectiveMaxTokens = textSettings?.enableThinking !== false
|
||||
? Math.max(baseMaxTokens, 1024)
|
||||
: baseMaxTokens;
|
||||
|
||||
const response = await streamChatWithLlm(requestMessages, {
|
||||
temperature: textSettings?.temperature || 0.7,
|
||||
maxTokens: textSettings?.maxTokens || 1024,
|
||||
maxTokens: effectiveMaxTokens,
|
||||
topP: textSettings?.topP,
|
||||
topK: textSettings?.topK,
|
||||
minP: textSettings?.minP,
|
||||
repeatPenalty: textSettings?.repeatPenalty,
|
||||
seed: textSettings?.seed,
|
||||
enableThinking: textSettings?.enableThinking !== false,
|
||||
signal: controller.signal,
|
||||
}, (_token, fullText, _reasoningToken, fullReasoning) => {
|
||||
const now = performance.now();
|
||||
@@ -530,7 +611,6 @@ function TextChat({
|
||||
tokensPerSecond: streamedTokens / generationSeconds,
|
||||
seconds: (now - requestStartedAt) / 1000,
|
||||
};
|
||||
|
||||
// rAF batching: accumulate updates and flush once per frame (~16ms)
|
||||
// This reduces React re-renders from N per token to ~60/sec
|
||||
tokenBufferRef.current = {
|
||||
@@ -583,6 +663,8 @@ function TextChat({
|
||||
tokens: exactTokens,
|
||||
tokensPerSecond: exactTokensPerSecond,
|
||||
seconds: exactSeconds,
|
||||
finishReason: response.finishReason || null,
|
||||
truncated: response.finishReason === "length",
|
||||
};
|
||||
|
||||
const processed = processMessageContent(assistantText, response.reasoningContent || assistantReasoning, textSettings?.enableThinking !== false);
|
||||
@@ -595,7 +677,23 @@ function TextChat({
|
||||
}];
|
||||
setMessages(finalMessages);
|
||||
saveConversationState(convId, finalMessages, selectedModel);
|
||||
if (response.usage) setTokenUsage(response.usage);
|
||||
const finalCompletionEstimate = Math.max(
|
||||
exactTokens,
|
||||
estimateTokens(processed.content) + estimateTokens(processed.reasoning)
|
||||
);
|
||||
const finalUsageEstimate = {
|
||||
prompt_tokens: promptTokenEstimate,
|
||||
completion_tokens: finalCompletionEstimate,
|
||||
total_tokens: promptTokenEstimate + finalCompletionEstimate,
|
||||
};
|
||||
setTokenUsage(response.usage
|
||||
? {
|
||||
prompt_tokens: Math.max(Number(response.usage.prompt_tokens) || 0, finalUsageEstimate.prompt_tokens),
|
||||
completion_tokens: Math.max(Number(response.usage.completion_tokens) || 0, finalUsageEstimate.completion_tokens),
|
||||
total_tokens: Math.max(Number(response.usage.total_tokens) || 0, finalUsageEstimate.total_tokens),
|
||||
}
|
||||
: finalUsageEstimate
|
||||
);
|
||||
|
||||
// Clean up attached files on success
|
||||
setAttachments([]);
|
||||
@@ -845,7 +943,13 @@ function TextChat({
|
||||
|
||||
{/* Generation stats pill */}
|
||||
{message.role === "assistant" && message.generationStats && !message.error && (
|
||||
<div className={`chat-generation-stats ${message.generationStats.status}`}>
|
||||
<>
|
||||
{message.generationStats.truncated && (
|
||||
<div className="chat-generation-warning">
|
||||
Response reached the token limit. Increase Max Response Tokens or ask "continue".
|
||||
</div>
|
||||
)}
|
||||
<div className={`chat-generation-stats ${message.generationStats.status}`}>
|
||||
{message.generationStats.status === "starting" ? (
|
||||
<><LoaderCircle size={11} className="progress-spinner" /> Waiting for first token...</>
|
||||
) : message.generationStats.status === "streaming" ? (
|
||||
@@ -853,7 +957,8 @@ function TextChat({
|
||||
) : (
|
||||
<>{message.generationStats.tokens} tokens <span style={{ opacity: 0.5 }}>•</span> {formatGenerationTime(message.generationStats.seconds)}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -932,13 +1037,7 @@ function TextChat({
|
||||
{status.ready && supportsThinking && (
|
||||
<button
|
||||
className={`chat-composer-deepthink-btn ${textSettings.enableThinking !== false ? "active" : ""}`}
|
||||
onClick={() => {
|
||||
const newVal = textSettings.enableThinking === false;
|
||||
setTextSettings(prev => ({
|
||||
...prev,
|
||||
enableThinking: newVal
|
||||
}));
|
||||
}}
|
||||
onClick={handleThinkingToggle}
|
||||
title={textSettings.enableThinking !== false ? "Disable DeepThink reasoning" : "Enable DeepThink reasoning"}
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
@@ -16,9 +16,11 @@ function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isS
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const formatGb = (value) => {
|
||||
const formatGb = (value, { allowZero = false } = {}) => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? number.toFixed(number >= 10 ? 0 : 1) : "--";
|
||||
if (!Number.isFinite(number) || number < 0) return "--";
|
||||
if (number === 0 && !allowZero) return "--";
|
||||
return number.toFixed(number >= 10 ? 0 : 1);
|
||||
};
|
||||
|
||||
const getStatusText = () => {
|
||||
@@ -128,7 +130,7 @@ function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isS
|
||||
{telemetry.vram_total_gb > 0 && (
|
||||
<div className="telemetry-chip" title={`${telemetry.gpu_name} VRAM`}>
|
||||
<Database className="telemetry-chip-icon" style={{ color: "var(--md-sys-color-tertiary)" }} />
|
||||
<span>VRAM: {formatGb(telemetry.vram_used_gb)} / {formatGb(telemetry.vram_total_gb)} GB</span>
|
||||
<span>VRAM: {formatGb(telemetry.vram_used_gb, { allowZero: true })} / {formatGb(telemetry.vram_total_gb)} GB</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -347,6 +347,16 @@ export async function startLlm(model, options = {}) {
|
||||
contextSize: options.contextSize,
|
||||
gpuLayers: options.gpuLayers,
|
||||
enableThinking: options.enableThinking,
|
||||
flashAttn: options.flashAttn,
|
||||
cacheTypeK: options.cacheTypeK,
|
||||
cacheTypeV: options.cacheTypeV,
|
||||
mlock: options.mlock,
|
||||
mmap: options.mmap,
|
||||
cachePrompt: options.cachePrompt,
|
||||
defragThold: options.defragThold,
|
||||
batchSize: options.batchSize,
|
||||
ubatchSize: options.ubatchSize,
|
||||
performanceProfile: options.performanceProfile,
|
||||
}),
|
||||
});
|
||||
return await readJsonResponse(res, "The local server returned an invalid text backend response.");
|
||||
@@ -423,6 +433,7 @@ export async function streamChatWithLlm(messages, options = {}, onToken = () =>
|
||||
let reasoningContent = "";
|
||||
let usage = null;
|
||||
let timings = null;
|
||||
let finishReason = null;
|
||||
|
||||
const consumeEvent = (eventText) => {
|
||||
try {
|
||||
@@ -435,8 +446,10 @@ export async function streamChatWithLlm(messages, options = {}, onToken = () =>
|
||||
if (!data || data === "[DONE]") return data === "[DONE]";
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
const token = parsed.choices?.[0]?.delta?.content || "";
|
||||
const reasoningToken = parsed.choices?.[0]?.delta?.reasoning_content || "";
|
||||
const choice = parsed.choices?.[0];
|
||||
const token = choice?.delta?.content || "";
|
||||
const reasoningToken = choice?.delta?.reasoning_content || "";
|
||||
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
||||
|
||||
if (token || reasoningToken) {
|
||||
content += token;
|
||||
@@ -470,7 +483,7 @@ export async function streamChatWithLlm(messages, options = {}, onToken = () =>
|
||||
|
||||
if (!finished && buffer.trim()) consumeEvent(buffer);
|
||||
if (!content && !reasoningContent) throw new Error("The text model returned an empty streamed response.");
|
||||
return { content, reasoningContent, usage, timings };
|
||||
return { content, reasoningContent, usage, timings, finishReason };
|
||||
}
|
||||
|
||||
export async function downloadLlmModel(url, filename = null) {
|
||||
|
||||
+133
-20
@@ -9,7 +9,7 @@ const fs = require("fs");
|
||||
const net = require("net");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawn, spawnSync, execSync, exec } = require("child_process");
|
||||
const { spawn, spawnSync, execSync, exec, execFile } = require("child_process");
|
||||
|
||||
// HTTP keep-alive agent for llama-server (eliminates TCP handshake per request)
|
||||
const llmHttpAgent = new http.Agent({
|
||||
@@ -697,6 +697,32 @@ function chooseAutoLayers(modelFilename, freeVramBytes, cacheTypeK = "q8_0", cac
|
||||
return Math.max(0, maxLayers);
|
||||
}
|
||||
|
||||
function getGgufModelSizeGb(modelFilename) {
|
||||
try {
|
||||
const modelPath = path.join(LLM_MODELS, modelFilename);
|
||||
if (fs.existsSync(modelPath)) {
|
||||
return fs.statSync(modelPath).size / (1024 * 1024 * 1024);
|
||||
}
|
||||
} catch (_) {}
|
||||
return 4;
|
||||
}
|
||||
|
||||
function getStableSyclGpuLayers(modelFilename, requestedLayers) {
|
||||
if (process.env.LOCALAI_LLM_ALLOW_FULL_SYCL === "1") return requestedLayers;
|
||||
if (requestedLayers !== -1) return requestedLayers;
|
||||
|
||||
const syclGpu = detectLlamaSyclGpu();
|
||||
const vramGb = syclGpu?.free_vram_gb || syclGpu?.vram_gb || 0;
|
||||
const modelSizeGb = getGgufModelSizeGb(modelFilename);
|
||||
if (vramGb > 0 && vramGb <= 8.5 && modelSizeGb >= 2.5) {
|
||||
return 0;
|
||||
}
|
||||
if (vramGb > 0 && vramGb <= 12 && modelSizeGb >= 5) {
|
||||
return 0;
|
||||
}
|
||||
return requestedLayers;
|
||||
}
|
||||
|
||||
let nvidiaSmiCmd = "nvidia-smi";
|
||||
let hasNvidiaSmi = null;
|
||||
|
||||
@@ -768,6 +794,77 @@ function getNvidiaVram() {
|
||||
return cachedVramInfo;
|
||||
}
|
||||
|
||||
let cachedLlamaVramInfo = null;
|
||||
let isPollingLlamaVram = false;
|
||||
let lastLlamaVramPollTime = 0;
|
||||
|
||||
function parseLlamaDeviceMemory(output) {
|
||||
const devices = [];
|
||||
const devicePattern = /(CUDA|Vulkan|SYCL)\d+\s*:\s*(.+?)\s+\(([\d.]+)\s+MiB(?:,\s*([\d.]+)\s+MiB free)?\)/gi;
|
||||
let match;
|
||||
while ((match = devicePattern.exec(output || "")) !== null) {
|
||||
const totalMiB = Number(match[3]);
|
||||
const freeMiB = match[4] ? Number(match[4]) : NaN;
|
||||
if (!Number.isFinite(totalMiB) || totalMiB <= 0) continue;
|
||||
|
||||
const usedMiB = Number.isFinite(freeMiB) ? Math.max(0, totalMiB - freeMiB) : 0;
|
||||
devices.push({
|
||||
gpu_name: match[2].trim(),
|
||||
vram_used_gb: Math.round((usedMiB / 1024) * 100) / 100,
|
||||
vram_total_gb: Math.round((totalMiB / 1024) * 100) / 100,
|
||||
});
|
||||
}
|
||||
return devices.sort((a, b) => b.vram_total_gb - a.vram_total_gb)[0] || null;
|
||||
}
|
||||
|
||||
function getLlamaTelemetryBackendPath() {
|
||||
if (osPlatform === "win32") {
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winVulkan)) return LLM_BACKEND_PATHS.winVulkan;
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winSycl)) return LLM_BACKEND_PATHS.winSycl;
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winCuda)) return LLM_BACKEND_PATHS.winCuda;
|
||||
} else if (osPlatform === "linux") {
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.linuxVulkan)) return LLM_BACKEND_PATHS.linuxVulkan;
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.linuxSycl)) return LLM_BACKEND_PATHS.linuxSycl;
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.linuxCuda)) return LLM_BACKEND_PATHS.linuxCuda;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function pollLlamaVram(force = false) {
|
||||
if (isPollingLlamaVram) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (!force && now - lastLlamaVramPollTime < 4500) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backendPath = getLlamaTelemetryBackendPath();
|
||||
if (!backendPath) return;
|
||||
|
||||
isPollingLlamaVram = true;
|
||||
lastLlamaVramPollTime = now;
|
||||
execFile(
|
||||
backendPath,
|
||||
["--list-devices"],
|
||||
{ windowsHide: true, timeout: 10000, maxBuffer: 1024 * 1024 },
|
||||
(error, stdout, stderr) => {
|
||||
isPollingLlamaVram = false;
|
||||
if (error) {
|
||||
cachedLlamaVramInfo = null;
|
||||
return;
|
||||
}
|
||||
cachedLlamaVramInfo = parseLlamaDeviceMemory(`${stdout || ""}\n${stderr || ""}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setInterval(() => pollLlamaVram(false), 5000);
|
||||
pollLlamaVram(true);
|
||||
|
||||
function getLlamaVram() {
|
||||
return cachedLlamaVramInfo;
|
||||
}
|
||||
|
||||
let cachedMacRamUsedGb = null;
|
||||
function pollMacRam() {
|
||||
if (osPlatform !== "darwin") return;
|
||||
@@ -963,6 +1060,7 @@ function getHardwareSpecs() {
|
||||
|
||||
function getTelemetry() {
|
||||
const vram = getNvidiaVram();
|
||||
const llamaVram = vram || getLlamaVram();
|
||||
let ram_used_gb = roundGb(os.totalmem() - os.freemem());
|
||||
if (osPlatform === "darwin" && cachedMacRamUsedGb !== null) {
|
||||
ram_used_gb = cachedMacRamUsedGb;
|
||||
@@ -972,9 +1070,9 @@ function getTelemetry() {
|
||||
cpu_usage: getCpuUsagePercent(),
|
||||
ram_used_gb,
|
||||
ram_total_gb: roundGb(os.totalmem()),
|
||||
gpu_name: vram?.gpu_name || gpu.name,
|
||||
vram_used_gb: vram?.vram_used_gb || 0,
|
||||
vram_total_gb: vram?.vram_total_gb || gpu.vram_gb || 0,
|
||||
gpu_name: llamaVram?.gpu_name || gpu.name,
|
||||
vram_used_gb: Number.isFinite(Number(llamaVram?.vram_used_gb)) ? llamaVram.vram_used_gb : 0,
|
||||
vram_total_gb: llamaVram?.vram_total_gb || gpu.vram_gb || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1189,21 +1287,21 @@ function getLlmBackend() {
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winCuda) && detectLlamaCudaGpu()) {
|
||||
return { path: LLM_BACKEND_PATHS.winCuda, mode: "Auto (CUDA/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winSycl) && detectLlamaSyclGpu()) {
|
||||
return { path: LLM_BACKEND_PATHS.winSycl, mode: "Auto (SYCL/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winVulkan) && detectLlamaVulkanGpu()) {
|
||||
return { path: LLM_BACKEND_PATHS.winVulkan, mode: "Auto (Vulkan/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winSycl) && detectLlamaSyclGpu()) {
|
||||
return { path: LLM_BACKEND_PATHS.winSycl, mode: "Auto (SYCL/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winCuda)) {
|
||||
return { path: LLM_BACKEND_PATHS.winCuda, mode: "Auto (CUDA/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winSycl)) {
|
||||
return { path: LLM_BACKEND_PATHS.winSycl, mode: "Auto (SYCL/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winVulkan)) {
|
||||
return { path: LLM_BACKEND_PATHS.winVulkan, mode: "Auto (Vulkan/CPU)" };
|
||||
}
|
||||
if (fs.existsSync(LLM_BACKEND_PATHS.winSycl)) {
|
||||
return { path: LLM_BACKEND_PATHS.winSycl, mode: "Auto (SYCL/CPU)" };
|
||||
}
|
||||
return { path: LLM_BACKEND_PATHS.winCpu, mode: "CPU" };
|
||||
} else if (osPlatform === "darwin") {
|
||||
return {
|
||||
@@ -2405,26 +2503,37 @@ async function startLlm(settings = {}) {
|
||||
contextSize = Math.max(512, Math.min(32768, contextSize));
|
||||
}
|
||||
|
||||
const isSyclBackend = backend.mode.includes("SYCL") || path.basename(path.dirname(backend.path)).toLowerCase() === "sycl";
|
||||
const requestedGpuLayers = Number.isFinite(Number(settings.gpuLayers)) ? Number(settings.gpuLayers) : -1;
|
||||
const effectiveGpuLayers = isSyclBackend ? getStableSyclGpuLayers(filename, requestedGpuLayers) : requestedGpuLayers;
|
||||
const effectiveFlashAttn = isSyclBackend ? false : settings.flashAttn !== false;
|
||||
const effectiveCacheTypeK = isSyclBackend ? "q4_0" : settings.cacheTypeK || llmSettings.cacheTypeK || "q8_0";
|
||||
const effectiveCacheTypeV = isSyclBackend ? "q4_0" : settings.cacheTypeV || llmSettings.cacheTypeV || "q8_0";
|
||||
const effectiveBatchSize = isSyclBackend ? Math.min(256, Number(settings.batchSize) || llmSettings.batchSize || 512) : Number(settings.batchSize) || llmSettings.batchSize || 512;
|
||||
const effectiveUbatchSize = isSyclBackend ? Math.min(256, Number(settings.ubatchSize) || llmSettings.ubatchSize || 512) : Number(settings.ubatchSize) || llmSettings.ubatchSize || 512;
|
||||
const effectiveEnableThinking = isSyclBackend && effectiveGpuLayers === 0 ? false : settings.enableThinking !== false;
|
||||
|
||||
llmSettings = {
|
||||
...llmSettings,
|
||||
model: filename,
|
||||
threads: Math.max(1, Math.min(64, Number(settings.threads) || llmSettings.threads)),
|
||||
contextSize: contextSize,
|
||||
gpuLayers: Number.isFinite(Number(settings.gpuLayers)) ? Number(settings.gpuLayers) : -1,
|
||||
gpuLayers: effectiveGpuLayers,
|
||||
requestedGpuLayers,
|
||||
backendMode: backend.mode,
|
||||
backendBinary: path.basename(backend.path),
|
||||
enableThinking: settings.enableThinking !== false,
|
||||
enableThinking: effectiveEnableThinking,
|
||||
supportsThinking: /deepseek|qwen3|gemma-4|gemma4|think|r1|e2b|reasoning/i.test(filename),
|
||||
// New performance settings
|
||||
flashAttn: settings.flashAttn !== false,
|
||||
cacheTypeK: settings.cacheTypeK || llmSettings.cacheTypeK || "q8_0",
|
||||
cacheTypeV: settings.cacheTypeV || llmSettings.cacheTypeV || "q8_0",
|
||||
flashAttn: effectiveFlashAttn,
|
||||
cacheTypeK: effectiveCacheTypeK,
|
||||
cacheTypeV: effectiveCacheTypeV,
|
||||
mlock: settings.mlock || llmSettings.mlock || false,
|
||||
mmap: settings.mmap !== false && llmSettings.mmap !== false,
|
||||
cachePrompt: settings.cachePrompt !== false && llmSettings.cachePrompt !== false,
|
||||
defragThold: Number(settings.defragThold) || llmSettings.defragThold || 0.1,
|
||||
batchSize: Math.max(128, Math.min(2048, Number(settings.batchSize) || llmSettings.batchSize || 512)),
|
||||
ubatchSize: Math.max(128, Math.min(2048, Number(settings.ubatchSize) || llmSettings.ubatchSize || 512)),
|
||||
batchSize: Math.max(128, Math.min(2048, effectiveBatchSize)),
|
||||
ubatchSize: Math.max(128, Math.min(2048, effectiveUbatchSize)),
|
||||
performanceProfile: settings.performanceProfile || llmSettings.performanceProfile || "balanced",
|
||||
};
|
||||
|
||||
@@ -2500,21 +2609,26 @@ async function startLlm(settings = {}) {
|
||||
"--model", modelPath,
|
||||
"--host", "127.0.0.1",
|
||||
"--port", String(PORT_LLM),
|
||||
"-lv", "1",
|
||||
"--ctx-size", String(llmSettings.contextSize),
|
||||
"--threads", String(llmSettings.threads),
|
||||
"--n-gpu-layers", String(llmSettings.gpuLayers),
|
||||
"--parallel", "1",
|
||||
"--flash-attn", "on", // ✅ Memory-efficient attention (all backends)
|
||||
"--cache-type-k", String(llmSettings.cacheTypeK), // ✅ KV cache quantization K
|
||||
"--cache-type-v", String(llmSettings.cacheTypeV), // ✅ KV cache quantization V
|
||||
"--cache-prompt", // ✅ Prompt caching for multi-turn (major UX win)
|
||||
"--ctx-checkpoints", "8", // ✅ Context shifting recovery
|
||||
"--poll", "30", // ✅ More aggressive polling
|
||||
"--metrics", // ✅ Metrics endpoint for tuning
|
||||
"--defrag-thold", String(llmSettings.defragThold), // ✅ KV cache defragmentation
|
||||
"--batch-size", String(llmSettings.batchSize), // ✅ Prompt processing batch
|
||||
"--ubatch-size", String(llmSettings.ubatchSize), // ✅ Micro-batch size
|
||||
];
|
||||
if (llmSettings.flashAttn) {
|
||||
args.push("--flash-attn", "on");
|
||||
}
|
||||
if (isSyclBackend) {
|
||||
args.push("--no-warmup");
|
||||
}
|
||||
|
||||
// CPU mode: lock memory to prevent OS paging (critical for consistent speed)
|
||||
const isGpuMode = backend.mode.includes("GPU") || backend.mode.includes("CUDA") ||
|
||||
@@ -2565,7 +2679,6 @@ async function startLlm(settings = {}) {
|
||||
|
||||
// SYCL-specific optimizations (Intel Arc/Graphics)
|
||||
if (backend.mode.includes("SYCL")) {
|
||||
spawnEnv.GGML_SYCL_ENABLE_FLASH_ATTN = "1";
|
||||
spawnEnv.ZES_ENABLE_SYSMAN = "1";
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user