From 3575bf7e6070f01d9486e0c3ee418f7f24aa91f1 Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Wed, 22 Jul 2026 11:12:40 +0300 Subject: [PATCH] release: v0.2.31 - O(1) attention knobs, reasoning toggle & admin token persistence --- CHANGELOG.md | 10 ++++ Cargo.toml | 2 +- src/admin/mod.rs | 3 ++ src/cmf_runtime.rs | 20 ++++++-- src/config.rs | 23 +++++++-- src/main.rs | 5 ++ web/js/ui.js | 4 ++ web/js/views/import.js | 5 +- web/js/views/models.js | 11 ++--- web/js/views/settings.js | 100 +++++++++++++++++++++++++++++---------- 10 files changed, 140 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fe135..a6dda1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## [0.2.31] - 2026-07-22 + +### Added +- **Structured O(1) Nyström Attention & Reasoning Control**: Enhanced settings drawer with structured parameter sections for O(1) Nyström Streaming Attention (`o1_m`, `o1_window`, `o1_sink`), Generation (Temp, Top-P, MaxTokens, System Prompt), Reasoning ("Disable reasoning" toggle mapping to `think_budget = 0` for Qwen3/3.5 models), and Runtime (`skip_mtp` via `CMF_MTP=0`). +- **Persistent Admin Token Configuration**: Support for explicit `token` field in `config/gateway.toml` under `[admin]` block to prevent authorization invalidation across restarts. + +### Fixed +- **JavaScript UI Helper Restoration**: Restored `opt(...)` helper in `web/js/ui.js` and `web/js/views/import.js` to ensure HF model import modal options render smoothly. +- **Engine Command Flag Accuracy**: Fixed `CMF_MTP=0` environment flag pass-through for `cortiq serve` processes. + ## [0.2.30] - 2026-07-22 ### Added diff --git a/Cargo.toml b/Cargo.toml index 50ba7da..d002cf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cortiq-gateway" -version = "0.2.30" +version = "0.2.31" edition = "2021" rust-version = "1.75" license = "Apache-2.0" diff --git a/src/admin/mod.rs b/src/admin/mod.rs index 014b08c..2a4166c 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -1336,6 +1336,9 @@ async fn register_import(State(state): State, Path(job): Path Result { if server.gpu { cmd.env("CMF_GPU", "1"); } + // O(1) Nyström attention: layers spec + sub-parameters. if let Some(o1) = &server.o1 { - if !o1.trim().is_empty() && o1 != "off" { - cmd.arg("--o1").arg(o1); + let s = o1.trim(); + if !s.is_empty() { + // Pass even "off" so it explicitly overrides a file converter hint. + cmd.arg("--o1").arg(s); } } + if let Some(m) = server.o1_m { + cmd.arg("--o1-m").arg(m.to_string()); + } + if let Some(w) = server.o1_window { + cmd.arg("--o1-window").arg(w.to_string()); + } + if let Some(sink) = server.o1_sink { + cmd.arg("--o1-sink").arg(sink.to_string()); + } + // MTP (Multi-Token Prediction) speculative decoding: the engine reads + // CMF_MTP env; "0" disables. There is no CLI flag for this. if server.skip_mtp { - cmd.arg("--skip-mtp"); + cmd.env("CMF_MTP", "0"); } cmd.stdout(Stdio::null()) .stderr(Stdio::null()) diff --git a/src/config.rs b/src/config.rs index 0b6a443..bd28697 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,10 +98,19 @@ pub struct CmfServer { /// Default system prompt prepended if request lacks one. #[serde(default, skip_serializing_if = "Option::is_none")] pub system_prompt: Option, - /// O(1) constant-memory streaming attention hint (all, deep, custom, off). + /// O(1) Nyström attention layer spec (all | deepN | i,j,k | off). #[serde(default, skip_serializing_if = "Option::is_none")] pub o1: Option, - /// Skip Multi-Token Prediction (MTP) heads. + /// O(1) landmark budget (default 32; ≥4). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub o1_m: Option, + /// O(1) exact-window width (default 128). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub o1_window: Option, + /// O(1) permanent exact sink keys (default 4, StreamingLLM discipline). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub o1_sink: Option, + /// Disable Multi-Token Prediction speculative decoding (CMF_MTP=0). #[serde(default)] pub skip_mtp: bool, } @@ -199,6 +208,9 @@ impl CmfCfg { think_budget: None, system_prompt: None, o1: None, + o1_m: None, + o1_window: None, + o1_sink: None, skip_mtp: false, }]; } @@ -510,10 +522,12 @@ pub struct AdminCfg { /// Whether the admin panel and admin API (`/admin`, `/admin/api/*`) are enabled. #[serde(default = "default_admin_enabled")] pub enabled: bool, - /// Name of the env variable holding the admin token. If unset (or empty), - /// a token is generated at startup and printed to the log. + /// Name of the env variable holding the admin token. #[serde(default, skip_serializing_if = "Option::is_none")] pub token_env: Option, + /// Persistent admin token configured directly in toml. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token: Option, /// Optional separate bind address for the admin surface (e.g. `127.0.0.1:9001`). /// If unset, the admin interface lives on the main `listen` address under `/admin`. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -524,6 +538,7 @@ impl Default for AdminCfg { Self { enabled: default_admin_enabled(), token_env: None, + token: None, listen: None, } } diff --git a/src/main.rs b/src/main.rs index 6c92bf1..56bb19b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,11 @@ fn resolve_admin_token(cfg: &Config, args: &Args) -> (String, bool) { return (t.clone(), false); } } + if let Some(t) = &cfg.admin.token { + if !t.is_empty() { + return (t.clone(), false); + } + } (admin::random_token(24), true) } diff --git a/web/js/ui.js b/web/js/ui.js index 93b82be..3e86fa6 100644 --- a/web/js/ui.js +++ b/web/js/ui.js @@ -23,6 +23,10 @@ export function clear(node) { while (node && node.firstChild) node.removeChild(node.firstChild); } +export function opt(v, label, sel) { + return h("option", { value: v, selected: sel ? true : null }, label || v); +} + export function mount(node, ...children) { clear(node); for (const c of children.flat()) diff --git a/web/js/views/import.js b/web/js/views/import.js index be38600..41f8256 100644 --- a/web/js/views/import.js +++ b/web/js/views/import.js @@ -2,7 +2,7 @@ // live progress view. Search HF, pick quantization + advanced params, watch // the conversion (multiple in parallel, resumable across tab switches), then // register the result as a local model. -import { h, mount, toast } from "../ui.js"; +import { h, mount, toast, opt } from "../ui.js"; import { t } from "../i18n.js"; import { api } from "../api.js"; @@ -31,9 +31,6 @@ function field(label, control, hint) { return h("label", { class: "field" }, h("span", {}, label), control, hint ? h("div", { class: "hint" }, hint) : null); } -function opt(v, label, sel) { - return h("option", { value: v, selected: sel ? true : null }, label || v); -} function fmtNum(n) { if (n == null) return "—"; if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"; diff --git a/web/js/views/models.js b/web/js/views/models.js index 88e26c8..25d4ed6 100644 --- a/web/js/views/models.js +++ b/web/js/views/models.js @@ -7,9 +7,6 @@ import { appState } from "../app.js"; function field(label, control, hint) { return h("label", { class: "field" }, h("span", {}, label), control, hint ? h("div", { class: "hint" }, hint) : null); } -function opt(v, label, sel) { - return h("option", { value: v, selected: sel ? true : null }, label || v); -} function keyBadge(src) { const map = { store: "store", env: "env", missing: "bad", none: "" }; @@ -124,10 +121,10 @@ function modelForm(meta, existing) { const maxTokIn = h("input", { type: "number", min: "1", placeholder: "2048", value: m.max_tokens ?? "" }); const thinkIn = h("input", { type: "number", min: "0", placeholder: "1024", value: m.think_budget ?? "" }); const o1Sel = h("select", {}, - opt("", t("import.adv.auto")), - opt("all", "all — O(1) Attention"), - opt("deep", "deep — O(1) Deep"), - opt("off", "off — Standard")); + h("option", { value: "" }, t("import.adv.auto")), + h("option", { value: "all" }, "all — O(1) Attention"), + h("option", { value: "deep" }, "deep — O(1) Deep"), + h("option", { value: "off" }, "off — Standard")); if (m.o1) o1Sel.value = m.o1; const skipMtpCb = h("input", { type: "checkbox", checked: m.skip_mtp ? true : null }); const sysPromptIn = h("textarea", { rows: 2, placeholder: t("models.form.systemPromptPlaceholder"), value: m.system_prompt || "" }); diff --git a/web/js/views/settings.js b/web/js/views/settings.js index 84b3ab1..eb02e98 100644 --- a/web/js/views/settings.js +++ b/web/js/views/settings.js @@ -253,33 +253,82 @@ export async function renderSettings() { } catch (e) { mount(st, String(e.message || e)); } checkBtn.disabled = false; }); - // ⚙ Model Generation & Thinking Parameters drawer - const tempIn = h("input", { type: "number", step: "0.05", min: "0", max: "2.0", placeholder: "0.7", value: sv.temperature ?? "", style: "width:65px" }); - const topPIn = h("input", { type: "number", step: "0.05", min: "0", max: "1.0", placeholder: "0.9", value: sv.top_p ?? "", style: "width:65px" }); - const maxTokIn = h("input", { type: "number", min: "1", placeholder: "2048", value: sv.max_tokens ?? "", style: "width:80px" }); - const thinkIn = h("input", { type: "number", min: "0", placeholder: "1024", value: sv.think_budget ?? "", style: "width:80px" }); - const o1Sel = h("select", { style: "width:110px" }, - opt("", "O(1) auto"), - opt("all", "O(1) all"), - opt("deep", "O(1) deep"), - opt("off", "O(1) off")); - if (sv.o1) o1Sel.value = sv.o1; - const skipMtpCb = h("input", { type: "checkbox", checked: sv.skip_mtp ? true : null }); - const sysIn = h("input", { placeholder: t("models.form.systemPromptPlaceholder"), value: sv.system_prompt || "", style: "flex:1" }); - const drawer = h("div", { class: "flex wrap gap-sm", style: "display:none;width:100%;margin-top:4px;padding:6px;background:var(--bg-subtle);border-radius:4px;align-items:center" }, - h("span", { class: "small muted" }, "Temp:"), tempIn, - h("span", { class: "small muted" }, "Top-P:"), topPIn, - h("span", { class: "small muted" }, "MaxTokens:"), maxTokIn, - h("span", { class: "small muted" }, "ThinkBudget:"), thinkIn, - o1Sel, - h("label", { class: "check small" }, skipMtpCb, "Skip MTP"), - h("span", { class: "small muted" }, "System:"), sysIn + // ── O(1) Nyström Attention ── + // NOT a "reasoning mode". This is a memory-efficient streaming attention + // kernel (Nyström/landmark) that replaces standard KV-cache attention, + // giving O(1) memory per decode step instead of O(n). + const o1Sel = h("select", { style: "width:120px", title: "O(1) Nyström attention layer spec: which layers replace KV-cache with streaming Nyström attention" }, + h("option", { value: "" }, "O(1) авто"), + h("option", { value: "all" }, "O(1) все слои"), + h("option", { value: "deep" }, "O(1) глубокие"), + h("option", { value: "off" }, "O(1) выкл.")); + if (sv.o1) o1Sel.value = sv.o1; + const o1mIn = h("input", { type: "number", min: "4", placeholder: "32", value: sv.o1_m ?? "", style: "width:55px", title: "Landmark budget (≥4, default 32). More landmarks ≠ better: m=64 measured worse." }); + const o1wIn = h("input", { type: "number", min: "1", placeholder: "128", value: sv.o1_window ?? "", style: "width:60px", title: "Exact-window width — main quality lever (default 128)" }); + const o1sinkIn = h("input", { type: "number", min: "0", placeholder: "4", value: sv.o1_sink ?? "", style: "width:50px", title: "Permanent exact sink keys, StreamingLLM discipline (default 4)" }); + + // ── Generation parameters ── + const tempIn = h("input", { type: "number", step: "0.05", min: "0", max: "2.0", placeholder: "0.7", value: sv.temperature ?? "", style: "width:60px" }); + const topPIn = h("input", { type: "number", step: "0.05", min: "0", max: "1.0", placeholder: "0.9", value: sv.top_p ?? "", style: "width:60px" }); + const maxTokIn = h("input", { type: "number", min: "1", placeholder: "2048", value: sv.max_tokens ?? "", style: "width:75px" }); + // think_budget = 0 means reasoning disabled (enable_thinking=false); + // null = model decides; N>0 = token budget for the block. + const noThinkCb = h("input", { type: "checkbox", checked: (sv.think_budget === 0) ? true : null, title: "Отключить режим размышлений (think_budget=0, enable_thinking=false для Qwen3/3.5)" }); + const thinkIn = h("input", { type: "number", min: "1", placeholder: "авто", value: (sv.think_budget && sv.think_budget > 0) ? sv.think_budget : "", style: "width:75px", title: "Бюджет токенов на размышления (think_budget). Пустое = модель решает сама." }); + if (sv.think_budget === 0) thinkIn.disabled = true; + noThinkCb.addEventListener("change", () => { + thinkIn.disabled = noThinkCb.checked; + if (noThinkCb.checked) thinkIn.value = ""; + }); + const sysIn = h("input", { placeholder: "System prompt (optional)", value: sv.system_prompt || "", style: "flex:1;min-width:200px" }); + + // ── MTP (Multi-Token Prediction): speculative decoding, NOT reasoning ── + const skipMtpCb = h("input", { type: "checkbox", checked: sv.skip_mtp ? true : null, title: "Disable MTP speculative decoding (env CMF_MTP=0)" }); + + // ── Settings drawer ── + const drawerStyle = "display:none;width:100%;margin-top:6px;padding:10px 12px;background:var(--bg-subtle);border-radius:6px;border:1px solid var(--border)"; + const groupStyle = "display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:4px 0"; + const labelStyle = "font-size:11px;color:var(--text-muted);font-weight:600;text-transform:uppercase;letter-spacing:0.5px;width:100%;margin-bottom:2px"; + + const drawer = h("div", { style: drawerStyle }, + // Group 1: O(1) Nyström Attention + h("div", { style: labelStyle }, "O(1) Nyström Attention"), + h("div", { style: groupStyle }, + h("span", { class: "small muted" }, "Слои:"), o1Sel, + h("span", { class: "small muted" }, "m:"), o1mIn, + h("span", { class: "small muted" }, "Window:"), o1wIn, + h("span", { class: "small muted" }, "Sink:"), o1sinkIn + ), + h("hr", { style: "border:none;border-top:1px solid var(--border);margin:6px 0" }), + // Group 2: Generation + h("div", { style: labelStyle }, "Генерация"), + h("div", { style: groupStyle }, + h("span", { class: "small muted" }, "Temp:"), tempIn, + h("span", { class: "small muted" }, "Top-P:"), topPIn, + h("span", { class: "small muted" }, "Max токенов:"), maxTokIn + ), + h("div", { style: groupStyle }, + h("span", { class: "small muted" }, "System prompt:"), sysIn + ), + h("hr", { style: "border:none;border-top:1px solid var(--border);margin:6px 0" }), + // Group 3: Reasoning (thinking) + h("div", { style: labelStyle }, "Режим размышлений"), + h("div", { style: groupStyle }, + h("label", { class: "check small", title: "Для моделей Qwen3/3.5: enable_thinking=false — модель отвечает напрямую без блока " }, noThinkCb, "Отключить размышления"), + h("span", { class: "small muted" }, "Think budget:"), thinkIn + ), + h("hr", { style: "border:none;border-top:1px solid var(--border);margin:6px 0" }), + // Group 4: Runtime + h("div", { style: labelStyle }, "Runtime"), + h("div", { style: groupStyle }, + h("label", { class: "check small", title: "MTP — Multi-Token Prediction (спекулятивное декодирование). Отключение замедляет генерацию, но стабилизирует output." }, skipMtpCb, "Отключить MTP") + ) ); - const gearBtn = h("button", { class: "btn small", type: "button", title: "⚙ " + t("models.form.paramsTitle") }, "⚙"); + const gearBtn = h("button", { class: "btn small", type: "button", title: "⚙ Настройки модели" }, "⚙"); gearBtn.addEventListener("click", () => { - drawer.style.display = drawer.style.display === "none" ? "flex" : "none"; + drawer.style.display = drawer.style.display === "none" ? "block" : "none"; }); const row = h("div", { class: "flex wrap", style: "gap:6px;align-items:center;margin:6px 0" }, @@ -293,8 +342,11 @@ export async function renderSettings() { temperature: tempIn.value !== "" ? parseFloat(tempIn.value) : null, top_p: topPIn.value !== "" ? parseFloat(topPIn.value) : null, max_tokens: maxTokIn.value !== "" ? parseInt(maxTokIn.value) : null, - think_budget: thinkIn.value !== "" ? parseInt(thinkIn.value) : null, + think_budget: noThinkCb.checked ? 0 : (thinkIn.value !== "" ? parseInt(thinkIn.value) : null), o1: o1Sel.value || null, + o1_m: o1mIn.value !== "" ? parseInt(o1mIn.value) : null, + o1_window: o1wIn.value !== "" ? parseInt(o1wIn.value) : null, + o1_sink: o1sinkIn.value !== "" ? parseInt(o1sinkIn.value) : null, skip_mtp: skipMtpCb.checked, system_prompt: sysIn.value.trim() || null, }) };