Strata dashboard plasmoid v1.2: barre a soglie e velocita decode/prefill

- card GPU con barre progressive: VRAM (informativa), utilizzo 60/85 %,
  temperatura 75/85 C, potenza su budget 350 W (ambra 70 %, rosso 90 %)
- velocita del motore: DEC (generazione) e PRE (prefill) in tok/s, live
  durante la generazione, ultima richiesta da fermo (badge LIVE/ULTIMA);
  semantica inversa: verde sopra la nominale, ambra fra 50 % e nominale
- card servizio + controllo unificata con switch load/unload nativo e
  stato che si chiude sulla fase osservata
- MetricBar condiviso con levelColor()/speedColor() e soglie in testa al QML
- README: requisiti, campi del control plane, tabella soglie, limiti del box
- avatar pixel-art 512x512 in assets/repository-avatar.png
This commit is contained in:
enne2
2026-10-25 16:12:52 +01:00
commit 2a0020ca91
8 changed files with 730 additions and 0 deletions
+581
View File
@@ -0,0 +1,581 @@
/* Strata (qwen3.8-flash-next) — stato e controllo del servizio di inferenza su seonse-engine-02.
*
* Adattato da llama-gsq3: ora parla con il control plane di Strata, che espone
* load/unload nativi (/load, /unload?wait=0). Strata serve UNA richiesta alla volta:
* la concorrenza e' solo informativa, non c'e' piu' il selettore di slot.
* Stile invariato rispetto agli altri plasmoidi enne2.
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.plasma5support as Plasma5Support
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
preferredRepresentation: fullRepresentation
switchWidth: 150
switchHeight: 150
readonly property color themeGreen: "#4ade80"
readonly property color themeIndigo: "#818cf8"
readonly property color themeViolet: "#a855f7"
// ---------- palette dei livelli ----------
readonly property color okColor: "#4ade80" // sotto la soglia di attenzione
readonly property color warnColor: "#fbbf24" // attenzione
readonly property color critColor: "#f87171" // critico
readonly property color naColor: "#5b6b7d" // valore non disponibile
// ---------- soglie GPU (ritocca qui) ----------
// Le barre PWR si riempiono sul budget: ambra oltre il 70 %, rosso oltre il 90 %.
// NVIDIA L40S: TDP 350 W, 48 GB; la VRAM resta informativa (colore accento).
readonly property real gpuPowerMax: 350
readonly property real utilWarn: 60
readonly property real utilCrit: 85
readonly property real tempWarn: 75
readonly property real tempCrit: 85
readonly property real powerWarnRatio: 0.70
readonly property real powerCritRatio: 0.90
// ---------- velocità di generazione e prefill (ritocca qui) ----------
// Nominali misurate sul L40S con MTP (2026-10-23): decode ~40 tok/s (picchi ~69),
// prefill ~2800-3800 tok/s. Fondo scala = massimo ragionevole.
readonly property real decodeNominal: 40
readonly property real decodeMax: 120
readonly property real prefillNominal: 3000
readonly property real prefillMax: 5000
readonly property string scripts: Qt.resolvedUrl("../scripts/").toString().replace(/^file:\/\//, "")
readonly property string pollSource: "/bin/bash " + root.scripts + "status.sh"
property var st: ({})
property string phase: "unknown"
property string errorMsg: ""
property string lastUpdate: ""
property bool pending: false
property string pendingTarget: ""
property int pendingTries: 0
property int counter: 0
property string actSource: ""
readonly property var modelInfo: root.st.model || ({})
readonly property var gpuInfo: root.st.gpu || ({})
readonly property var liveInfo: root.modelInfo.live || ({})
readonly property var lastTimings: root.modelInfo.last_timings || ({})
readonly property bool speedsLive: typeof root.liveInfo.tok_s === "number"
// durante la generazione valgono i valori live, da idle quelli dell'ultima richiesta
readonly property real decodeTokS: (typeof root.liveInfo.tok_s === "number")
? root.liveInfo.tok_s : root.metric(root.lastTimings.predicted_per_second)
readonly property real prefillTokS: (typeof root.liveInfo.prefill_tok_s_mean === "number")
? root.liveInfo.prefill_tok_s_mean : root.metric(root.lastTimings.prompt_per_second)
readonly property var concInfo: root.modelInfo.concurrency || ({})
readonly property bool loaded: root.modelInfo.loaded === true
readonly property bool running: root.pending ? (root.pendingTarget === "load")
: (root.phase === "loaded" || root.phase === "generating" || root.phase === "loading")
Plasmoid.busy: root.pending
toolTipMainText: "Strata — " + root.phaseWord()
toolTipSubText: {
const m = root.modelInfo, g = root.gpuInfo
if (root.errorMsg.length > 0) return root.errorMsg
return "fase: " + root.phase
+ "\nmodello: " + (m.alias || "—") + (m.kv ? " (" + m.kv + ")" : "")
+ "\ncontesto: " + root.ctxLabel() + " per richiesta"
+ "\nconcorrenza: " + (root.concInfo.serving || 1) + " · vision: " + (m.vision ? "attiva" : "non disponibile")
+ "\nVRAM: " + (g.mem_used_mib / 1024).toFixed(1) + " / " + (g.mem_total_mib / 1024).toFixed(1) + " GiB"
+ "\nutil " + (g.util_pct || 0).toFixed(0) + "% · " + (g.temp_c || 0).toFixed(0) + " °C · " + (g.power_w || 0).toFixed(0) + " W"
+ "\ndecode " + root.fmt(root.decodeTokS, 1, " tok/s") + " · prefill " + root.fmt(root.prefillTokS, 0, " tok/s")
+ (root.speedsLive ? " (live)" : " (ultima richiesta)")
}
function phaseAccent() {
switch (root.phase) {
case "loaded": return root.themeGreen
case "generating": return root.themeIndigo
case "loading": return root.themeViolet
case "unreachable": return Kirigami.Theme.negativeTextColor
default: return Kirigami.Theme.disabledTextColor
}
}
function phaseWord() {
if (root.pending) return root.pendingTarget === "load" ? "CARICAMENTO" : "SCARICAMENTO"
switch (root.phase) {
case "loaded": return "IN MEMORIA"
case "generating": return "GENERAZIONE"
case "loading": return "CARICAMENTO"
case "unloaded": return "SCARICATO"
case "stopped": return "SERVIZIO FERMO"
case "unreachable": return "NON RAGGIUNGIBILE"
default: return "?"
}
}
function phaseBadge() {
if (root.pending) return "ATTENDI"
switch (root.phase) {
case "loaded": return "IDLE"
case "generating": return "GEN"
case "loading": return "LOAD"
case "unloaded": return "OFF"
case "stopped": return "STOP"
default: return "ERR"
}
}
function stateLine() {
const m = root.modelInfo
if (root.phase === "generating" && m.tok_s) return "in generazione · " + Number(m.tok_s).toFixed(1) + " t/s"
switch (root.phase) {
case "loaded": return "caricato, in attesa"
case "loading": return "caricamento in corso…"
case "unloaded": return "scaricato · VRAM libera"
case "stopped": return "container fermo"
default: return root.errorMsg.length > 0 ? root.errorMsg : "stato sconosciuto"
}
}
function ctxLabel() {
const c = root.modelInfo.ctx
if (!c) return "—"
return c >= 1024 ? Math.round(c / 1024) + "k" : String(c)
}
function gib(v) { return (typeof v === "number") ? (v / 1024).toFixed(1) : "—" }
function num(v, d) { return (typeof v === "number") ? v : (d || 0) }
// valore numerico o NaN (per le barre: NaN -> grigio e barra vuota)
function metric(v) { return (typeof v === "number") ? v : NaN }
function fmt(v, d, suffix) { return (typeof v === "number") ? v.toFixed(d === undefined ? 0 : d) + (suffix || "") : "n/d" }
// Colore semaforico di una barra: NaN -> grigio, poi verde/ambra/rosso.
function levelColor(value, warnAt, critAt) {
if (isNaN(value)) return root.naColor
if (value >= critAt) return root.critColor
if (value >= warnAt) return root.warnColor
return root.okColor
}
// Frazione 0..1 di riempimento.
function fillRatio(value, scaleMax) {
if (isNaN(value) || !(scaleMax > 0)) return 0
return Math.max(0, Math.min(1, value / scaleMax))
}
// Per le velocità la semantica è inversa a quella dei carichi: alto = bene.
function speedColor(value, nominal) {
if (isNaN(value) || value <= 0) return root.naColor
if (value >= nominal) return root.okColor
if (value >= nominal * 0.5) return root.warnColor
return root.critColor
}
// ---------- polling ----------
Plasma5Support.DataSource {
id: poll
engine: "executable"
interval: 4000
connectedSources: [root.pollSource]
onNewData: function (source, data) {
const out = ((data && data["stdout"]) || "").trim()
if (out.length < 2) { root.errorMsg = "nessuna risposta dal control plane"; root.phase = "unreachable"; return }
let o
try { o = JSON.parse(out) } catch (e) { root.errorMsg = "risposta non valida"; return }
if (o.error) { root.errorMsg = o.error; root.phase = "unreachable"; return }
root.errorMsg = ""
root.st = o
root.phase = o.phase || "unknown"
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
root.checkPending()
}
}
// ---------- azioni ----------
Plasma5Support.DataSource {
id: act
engine: "executable"
connectedSources: []
onNewData: function (source, data) { if (source === root.actSource) root.clearAction() }
}
function clearAction() {
if (root.actSource.length > 0) { try { act.disconnectSource(root.actSource) } catch (e) {} }
root.actSource = ""
}
function runAction(cmd, target) {
root.counter++
root.pending = true
root.pendingTarget = target
root.pendingTries = 0
root.actSource = "/bin/bash " + root.scripts + "action.sh " + cmd + " " + root.counter
act.connectSource(root.actSource)
pendingTimer.restart()
}
// criterio autorevole: la fase osservata, non il flag locale
function checkPending() {
if (!root.pending) return
const done = (root.pendingTarget === "load" &&
(root.phase === "loaded" || root.phase === "generating"))
|| (root.pendingTarget === "unload" &&
(root.phase === "unloaded" || root.phase === "stopped"))
if (done) { root.pending = false; pendingTimer.stop() }
}
Timer {
id: pendingTimer
interval: 1000
repeat: true
onTriggered: {
root.pendingTries++
if (root.pendingTries > 240) { // 240 s: il load da freddo puo' durare ~160 s
root.pending = false
root.errorMsg = "operazione non confermata (timeout)"
stop()
}
}
}
// Barra progressiva con colore a soglie: si riempie su value/scaleMax,
// colora verde -> ambra -> rosso in base a warnAt/critAt.
component MetricBar: RowLayout {
id: bar
property string label
property real value: NaN
property real scaleMax: 100
property real warnAt: 60
property real critAt: 85
property string display
// opzionale: metriche dove il valore alto è positivo (velocità)
property bool higherIsBetter: false
property real nominal: 1
readonly property real ratio: root.fillRatio(bar.value, bar.scaleMax)
readonly property color tint: bar.higherIsBetter
? root.speedColor(bar.value, bar.nominal)
: root.levelColor(bar.value, bar.warnAt, bar.critAt)
Layout.fillWidth: true
spacing: 5
PC3.Label {
text: bar.label
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 4
Layout.preferredWidth: 28
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 5
Layout.alignment: Qt.AlignVCenter
Rectangle {
anchors.fill: parent
radius: 2.5
color: Qt.rgba(1, 1, 1, 0.12)
Rectangle {
width: parent.width * bar.ratio
height: parent.height
radius: 2.5
color: bar.tint
Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 300 } }
}
}
}
PC3.Label {
text: bar.display
color: isNaN(bar.value) ? root.naColor : bar.tint
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
horizontalAlignment: Text.AlignRight
Layout.preferredWidth: 52
Layout.alignment: Qt.AlignVCenter
}
}
compactRepresentation: Component {
Item {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
Kirigami.Icon {
anchors.fill: parent
source: "utilities-system-monitor"
color: root.phaseAccent()
}
MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded }
}
}
fullRepresentation: Component {
Item {
implicitWidth: 256
implicitHeight: 248
Layout.preferredWidth: 256
Layout.preferredHeight: 248
ColumnLayout {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(parent.width - 16, 300)
spacing: 2
// header
RowLayout {
Layout.fillWidth: true
spacing: 6
Kirigami.Icon {
source: "utilities-system-monitor"
color: root.phaseAccent()
Layout.preferredWidth: 20
Layout.preferredHeight: 20
}
RowLayout {
spacing: 6
Layout.fillWidth: true
PC3.Label {
text: "STRATA"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
}
PC3.Label {
text: root.phaseWord()
color: root.phaseAccent()
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
}
Item { Layout.fillWidth: true }
Rectangle {
Layout.preferredWidth: 8
Layout.preferredHeight: 8
radius: 4
color: root.phaseAccent()
}
PC3.Button {
icon.name: "view-refresh"
display: QQC2.AbstractButton.IconOnly
enabled: !root.pending
onClicked: { try { poll.disconnectSource(root.pollSource); poll.connectSource(root.pollSource) } catch (e) {} }
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Qt.rgba(root.phaseAccent().r, root.phaseAccent().g, root.phaseAccent().b, 0.35)
}
PC3.Label {
visible: root.errorMsg.length > 0
Layout.fillWidth: true
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
color: Kirigami.Theme.neutralTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
text: root.errorMsg
}
// card GPU
Rectangle {
Layout.fillWidth: true
implicitHeight: gpuBody.implicitHeight + 10
radius: 5
color: Qt.rgba(0, 0, 0, 0.28)
border.width: 1
border.color: Qt.rgba(root.themeIndigo.r, root.themeIndigo.g, root.themeIndigo.b, 0.65)
ColumnLayout {
id: gpuBody
anchors.fill: parent
anchors.margins: 4
spacing: 3
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: "GPU"
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: String(root.gpuInfo.name || "").replace("NVIDIA ", "")
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: root.speedsLive ? "LIVE" : "ULTIMA"
color: root.speedsLive ? root.themeGreen : Kirigami.Theme.disabledTextColor
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 4
}
Item { Layout.fillWidth: true }
PC3.Label {
text: root.gib(root.gpuInfo.mem_used_mib) + " / " + root.gib(root.gpuInfo.mem_total_mib) + " GiB"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 6
radius: 3
color: Qt.rgba(1, 1, 1, 0.12)
Rectangle {
width: parent.width * Math.min(1, Math.max(0, root.num(root.gpuInfo.mem_pct, 0) / 100))
height: parent.height
radius: 3
color: root.themeIndigo
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
}
}
MetricBar {
label: "UTIL"
value: root.metric(root.gpuInfo.util_pct)
scaleMax: 100
warnAt: root.utilWarn
critAt: root.utilCrit
display: root.fmt(root.gpuInfo.util_pct, 0, "%")
}
MetricBar {
label: "TEMP"
value: root.metric(root.gpuInfo.temp_c)
scaleMax: 100
warnAt: root.tempWarn
critAt: root.tempCrit
display: root.fmt(root.gpuInfo.temp_c, 0, " °C")
}
MetricBar {
label: "PWR"
value: root.metric(root.gpuInfo.power_w)
scaleMax: root.gpuPowerMax
warnAt: root.gpuPowerMax * root.powerWarnRatio
critAt: root.gpuPowerMax * root.powerCritRatio
display: root.fmt(root.gpuInfo.power_w, 0, " W")
}
// velocità del motore: decode (generazione) e prefill, tok/s
MetricBar {
label: "DEC"
value: root.decodeTokS
scaleMax: root.decodeMax
nominal: root.decodeNominal
higherIsBetter: true
display: root.fmt(root.decodeTokS, 1, " t/s")
}
MetricBar {
label: "PRE"
value: root.prefillTokS
scaleMax: root.prefillMax
nominal: root.prefillNominal
higherIsBetter: true
display: root.fmt(root.prefillTokS, 0, " t/s")
}
}
}
// card servizio + controllo (load/unload nativo)
Rectangle {
Layout.fillWidth: true
implicitHeight: svcBody.implicitHeight + 10
radius: 5
color: Qt.rgba(0, 0, 0, 0.28)
border.width: 1
border.color: Qt.rgba(root.phaseAccent().r, root.phaseAccent().g, root.phaseAccent().b, 0.65)
ColumnLayout {
id: svcBody
anchors.fill: parent
anchors.margins: 4
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 4
PC3.Label {
text: "SERVIZIO"
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: root.phaseBadge()
color: root.phaseAccent()
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.BusyIndicator {
visible: root.pending
running: root.pending
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
PC3.Switch {
checked: root.running
enabled: !root.pending && root.phase !== "unreachable" && root.phase !== "stopped"
onToggled: {
root.runAction(checked ? "load" : "unload", checked ? "load" : "unload")
checked = Qt.binding(function () { return root.running })
}
QQC2.ToolTip.text: root.running ? "Scarica il modello (libera la VRAM)" : "Carica il modello in VRAM"
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: 500
}
}
PC3.Label {
Layout.fillWidth: true
text: root.modelInfo.alias || "—"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
elide: Text.ElideRight
}
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: (root.modelInfo.kv ? root.modelInfo.kv + " KV" : "—") + " · ctx " + root.ctxLabel()
+ " · " + (root.concInfo.serving || 1) + "x"
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: root.modelInfo.vision ? "vision ✓" : "vision —"
color: root.modelInfo.vision ? root.themeGreen : Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
PC3.Label {
Layout.fillWidth: true
text: root.stateLine()
color: root.pending ? root.themeViolet : Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 4
elide: Text.ElideRight
}
}
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: root.lastUpdate ? "SYNC " + root.lastUpdate : "SYNC…"
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: root.pending ? "PENDING" : root.phaseBadge()
color: root.phaseAccent()
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
}
}