Skip to content

Commit 8a714d5

Browse files
committed
chore(release): v0.4.4 — Fit-Korean custom model + Phase B/C lever wiring
## New custom HF model Gemma 4 — 26B A4B Fit-Korean (custom, hsng95) hsng95/gemma-4-26b-a4b-mlx-fit-korean 11 GB / 5.057 bpw base / per-layer 79~117 experts (vs 128 source) Flagship-KR (HIGH=6 + top4=0.40) with 961 dormant MoE cells (25%) pruned via workload-specific imatrix routing analysis. Korean axes preserved within stderr (HAERAE 0.683 vs Flagship 0.687, ±0.015); English commonsense within stderr (HellaSwag-norm 0.395 vs 0.410). Trade-off: English reasoning/factual −13~14% (ARC, TruthfulQA), math CoT −80% (GSM8K) — Korean-first deployment trade-off. Catalog entry: "Gemma 4 — 26B A4B Fit-Korean (custom, hsng95)" in lumen-server, 24 GB min RAM (Mac mini M4 Pro stable). ## Phase B/C lever wiring (landed in prior sessions, now committed) * native_cache.rs +3 truncate_to impls for NativeKvCacheQuantized / NativeRotatingKvCacheQuantized / NativeRotatingKvCacheTurboQuant — unblocks QUANT_KV + prefix cache co-existence (TurboQuant attention path requirement for Phase 3 rvLLM PR). * gemma4_moe.rs: dispatch the new truncate_to from NativeGemma4PromptCache; LUMEN_GEMMA4_TOP_K env override for per-decode top_k tuning (default behavior unchanged; opt-in env). * gemma4_backend.rs: MTP chat path gated default-OFF behind LUMEN_GEMMA4_MTP=1 — Apple Silicon batch=1 MTP n_draft=6 measured NET LOSS on conversational workloads; OFF preserves perf-tuned custom-FA-2 kernel. Greedy bit-identical NOT guaranteed between OFF/ON paths (by-design kernel selection difference). * gemma4_backend_prefix_cache_smoke.rs: stale 6-arg call updated to 8-arg API (tools + tool_choice now required). * 3 new examples: - gemma4_backend_mtp_chat_smoke.rs (MTP chat A/B harness) - gemma4_pruned_token_dump.rs (greedy token A/B for pruned vs source verification) - gemma4_english_team_recognition.rs (English vocab recognition smoke for pruned builds) * cargo fmt cleanup across lumen-app, lumen-model. ## Version bump crates/lumen-app/Cargo.toml + tauri.conf.json + frontend/package.json — 0.4.3 → 0.4.4. Cargo.lock refreshed. OTA auto-update inherits.
1 parent 0897af3 commit 8a714d5

15 files changed

Lines changed: 675 additions & 27 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lumen-app/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lumen-app"
3-
version = "0.4.3"
3+
version = "0.4.4"
44
edition.workspace = true
55
rust-version.workspace = true
66
license.workspace = true

crates/lumen-app/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "lumen-app-frontend",
33
"private": true,
4-
"version": "0.4.3",
4+
"version": "0.4.4",
55
"type": "module",
66
"scripts": {
77
"predev": "cargo build --manifest-path ../../../Cargo.toml -p lumen-server --release",

crates/lumen-app/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ fn main() {
3838
};
3939
match hf_token_src {
4040
Some(name) => {
41-
eprintln!("[lumen-app] HuggingFace token detected via ${name} — gated repos accessible");
41+
eprintln!(
42+
"[lumen-app] HuggingFace token detected via ${name} — gated repos accessible"
43+
);
4244
}
4345
None => {
4446
eprintln!(

crates/lumen-app/src/models.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ pub fn hf_client(timeout: Option<std::time::Duration>) -> Result<reqwest::Client
465465
.or_else(|| std::env::var("HUGGING_FACE_HUB_TOKEN").ok())
466466
.map(|t| t.trim().to_string())
467467
.filter(|t| !t.is_empty());
468-
let mut builder = reqwest::Client::builder()
469-
.user_agent(concat!("lumen-app/", env!("CARGO_PKG_VERSION")));
468+
let mut builder =
469+
reqwest::Client::builder().user_agent(concat!("lumen-app/", env!("CARGO_PKG_VERSION")));
470470
if let Some(d) = timeout {
471471
builder = builder.timeout(d);
472472
}

crates/lumen-app/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Lumen",
4-
"version": "0.4.3",
4+
"version": "0.4.4",
55
"identifier": "ai.lumen.app",
66
"build": {
77
"frontendDist": "frontend/dist",
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//! End-to-end smoke for `Gemma4Backend::chat_streaming` with the MTP
2+
//! decode branch wired through `mtp_step()` (Phase 3 landed 2026-05-24).
3+
//!
4+
//! Two runs against the same chat-templated prompt:
5+
//! 1. `LUMEN_GEMMA4_MTP=0` (baseline, async-pipelined per-token loop)
6+
//! 2. `LUMEN_GEMMA4_MTP=1` (routes through mtp_step → committed batch)
7+
//!
8+
//! At temperature=0 (greedy), Google's assistant-drafter MTP guarantees
9+
//! byte-identical visible output. We assert the two runs produce the same
10+
//! `ParsedResponse.visible` string and log per-run wall-clock for an A/B
11+
//! comparison.
12+
//!
13+
//! Run:
14+
//! MLX_LOCAL_SOURCE_DIR=/Users/sonheesung/Documents/GitHub/mlx \
15+
//! MODEL_ID=/Users/sonheesung/models/hsng95--gemma-4-26b-a4b-mlx-imatrix3plus-awq \
16+
//! DRAFTER_DIR=/Users/sonheesung/models/gemma-4-26B-A4B-it-assistant-bf16 \
17+
//! cargo run --release -p lumen-mlx \
18+
//! --example gemma4_backend_mtp_chat_smoke --features mlx-native
19+
20+
use std::path::Path;
21+
use std::time::Instant;
22+
23+
use anyhow::{Context, Result};
24+
25+
#[cfg(feature = "mlx-native")]
26+
use lumen_mlx::chat_io::{BackendStreamEvent, ResolvedToolChoice};
27+
#[cfg(feature = "mlx-native")]
28+
use lumen_mlx::gemma4::{Gemma4Backend, ToolDef};
29+
30+
#[cfg(feature = "mlx-native")]
31+
fn run_once(
32+
backend: &mut Gemma4Backend,
33+
messages: &[(String, String)],
34+
max_new_tokens: usize,
35+
tools: &[ToolDef<'_>],
36+
tool_choice: &ResolvedToolChoice<'_>,
37+
label: &str,
38+
) -> Result<(String, f64)> {
39+
let mut buf = String::new();
40+
let t0 = Instant::now();
41+
let resp = backend.chat_streaming(
42+
messages,
43+
max_new_tokens,
44+
/* temperature */ 0.0,
45+
/* top_p */ 1.0,
46+
/* thinking */ false,
47+
tools,
48+
tool_choice,
49+
|ev| {
50+
if let BackendStreamEvent::Text(t) = ev {
51+
buf.push_str(t);
52+
}
53+
Ok(())
54+
},
55+
)?;
56+
let wall_ms = t0.elapsed().as_secs_f64() * 1e3;
57+
eprintln!(
58+
"[mtp-chat-smoke:{label}] visible={:?} wall={:.0}ms",
59+
resp.visible, wall_ms
60+
);
61+
Ok((resp.visible, wall_ms))
62+
}
63+
64+
#[cfg(feature = "mlx-native")]
65+
fn main() -> Result<()> {
66+
let model_id = std::env::var("MODEL_ID").unwrap_or_else(|_| {
67+
"/Users/sonheesung/models/hsng95--gemma-4-26b-a4b-mlx-imatrix3plus-awq".into()
68+
});
69+
let drafter_dir = std::env::var("DRAFTER_DIR")
70+
.unwrap_or_else(|_| "/Users/sonheesung/models/gemma-4-26B-A4B-it-assistant-bf16".into());
71+
let max_new_tokens: usize = std::env::var("MAX_NEW_TOKENS")
72+
.ok()
73+
.and_then(|s| s.parse().ok())
74+
.unwrap_or(256);
75+
76+
eprintln!("[mtp-chat-smoke] loading trunk {model_id}");
77+
let mut backend = Gemma4Backend::from_dir("gemma4-mtp-chat-smoke", Path::new(&model_id))
78+
.context("backend load")?;
79+
80+
eprintln!("[mtp-chat-smoke] enabling MTP from {drafter_dir}");
81+
let enabled = backend
82+
.try_enable_mtp(Path::new(&drafter_dir))
83+
.context("try_enable_mtp")?;
84+
if !enabled {
85+
return Err(anyhow::anyhow!(
86+
"try_enable_mtp returned false (backbone_hidden_size mismatch)"
87+
));
88+
}
89+
90+
let messages = vec![
91+
(
92+
"system".to_string(),
93+
"당신은 한국어로 정중히 답하는 어시스턴트입니다.".to_string(),
94+
),
95+
(
96+
"user".to_string(),
97+
"한국의 4대 명승지를 각각 한 문단씩 설명해주세요. 역사, 위치, 특징을 포함해서.".to_string(),
98+
),
99+
];
100+
101+
let tools: &[ToolDef<'_>] = &[];
102+
let tool_choice = ResolvedToolChoice::Auto;
103+
104+
// ── Warmup pass (MTP off, throwaway) so MLX kernel / graph cache and
105+
// native runner fast-mode are warmed before we measure either path.
106+
// Without this, the OFF run pays cold-start cost and the ON run
107+
// inherits the warmth — net 8× per-token regression in the OFF
108+
// measurement vs documented 73 tok/s steady state. ──
109+
unsafe {
110+
std::env::set_var("LUMEN_GEMMA4_MTP", "0");
111+
}
112+
eprintln!("[mtp-chat-smoke] warmup pass (max_new_tokens=16)");
113+
let _ = run_once(&mut backend, &messages, 16, tools, &tool_choice, "warmup")?;
114+
115+
// ── Run 1: MTP OFF ──
116+
unsafe {
117+
std::env::set_var("LUMEN_GEMMA4_MTP", "0");
118+
}
119+
let (visible_off, wall_off) = run_once(
120+
&mut backend,
121+
&messages,
122+
max_new_tokens,
123+
tools,
124+
&tool_choice,
125+
"off",
126+
)?;
127+
128+
// ── Run 2: MTP ON ──
129+
unsafe {
130+
std::env::set_var("LUMEN_GEMMA4_MTP", "1");
131+
}
132+
let (visible_on, wall_on) = run_once(
133+
&mut backend,
134+
&messages,
135+
max_new_tokens,
136+
tools,
137+
&tool_choice,
138+
"on",
139+
)?;
140+
141+
println!("\n=== Gemma4Backend MTP chat A/B ===");
142+
println!("max_new_tokens = {max_new_tokens}");
143+
println!("OFF visible: {:?}", visible_off);
144+
println!("ON visible: {:?}", visible_on);
145+
println!("OFF wall: {:.0} ms", wall_off);
146+
println!("ON wall: {:.0} ms", wall_on);
147+
let speedup = if wall_on > 0.0 {
148+
wall_off / wall_on
149+
} else {
150+
0.0
151+
};
152+
println!("speedup ON / OFF = {:.2}x (wall-clock)", speedup);
153+
154+
if visible_off != visible_on {
155+
eprintln!(
156+
"WARNING: MTP-on visible differs from MTP-off — greedy bit-identical guarantee violated"
157+
);
158+
eprintln!(" off len chars = {}", visible_off.chars().count());
159+
eprintln!(" on len chars = {}", visible_on.chars().count());
160+
} else {
161+
println!("\n✓ bit-identical visible string (greedy guarantee holds)");
162+
}
163+
164+
Ok(())
165+
}
166+
167+
#[cfg(not(feature = "mlx-native"))]
168+
fn main() {
169+
eprintln!("build with --features mlx-native to run this example");
170+
}

crates/lumen-mlx/examples/gemma4_backend_prefix_cache_smoke.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use anyhow::{Context, Result};
2121

2222
#[cfg(feature = "mlx-native")]
2323
fn main() -> Result<()> {
24-
use lumen_mlx::gemma4::Gemma4Backend;
24+
use lumen_mlx::chat_io::ResolvedToolChoice;
25+
use lumen_mlx::gemma4::{Gemma4Backend, ToolDef};
2526

2627
let model_id = std::env::var("MODEL_ID")
2728
.unwrap_or_else(|_| "/path/to/models/gemma-4-26b-a4b-mlx-3bit".into());
@@ -62,6 +63,8 @@ fn main() -> Result<()> {
6263

6364
let max_new_tokens = 200;
6465
let prefix_key = "moltis-sports-batch-001";
66+
let tools: &[ToolDef<'_>] = &[];
67+
let tool_choice = ResolvedToolChoice::Auto;
6568

6669
println!("\n=== Gemma4Backend prefix_cache end-to-end smoke ===\n");
6770
for (i, query) in queries.iter().enumerate() {
@@ -79,6 +82,8 @@ fn main() -> Result<()> {
7982
/* top_p */ 1.0,
8083
/* thinking */ false,
8184
prefix_key,
85+
tools,
86+
&tool_choice,
8287
)
8388
.with_context(|| format!("request {} failed", i + 1))?;
8489
let wall_ms = t0.elapsed().as_secs_f64() * 1e3;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Verify pruned Gemma 4 recognizes English sport team / league names.
2+
//!
3+
//! Probes the Moltis matching workload: Korean query → JSON tool call
4+
//! using English team & league names. The token output is decoded and
5+
//! printed for both source & pruned model comparison.
6+
//!
7+
//! Run:
8+
//! MODEL_ID=/path/to/model cargo run --release --features mlx-native \
9+
//! -p lumen-mlx --example gemma4_english_team_recognition
10+
11+
use std::path::Path;
12+
13+
use anyhow::{Context, Result};
14+
15+
#[cfg(feature = "mlx-native")]
16+
fn main() -> Result<()> {
17+
use lumen_mlx::gemma4::{GenerateConfig, NativeGemma4Model};
18+
19+
// 4 Moltis-style queries each naming a different English team. The
20+
// model should produce a JSON tool_call containing the canonical
21+
// English team name + league name. Each prompt is chat-templated.
22+
let queries: &[(&str, &[u32])] = &[
23+
// "Liverpool 이 어느 리그 소속인지 알려줘"
24+
(
25+
"Liverpool",
26+
&[
27+
2, 105, 2364, 107, 98125, 4214, 107561, 225183, 18004, 238701, 93860, 88440,
28+
242332, 106, 107, 105, 4368, 107, 100, 45518, 107, 101,
29+
],
30+
),
31+
// "Real Madrid 가 어느 리그?"
32+
(
33+
"Real Madrid",
34+
&[
35+
2, 105, 2364, 107, 20235, 19627, 8486, 107561, 225183, 236881, 106, 107, 105, 4368,
36+
107, 100, 45518, 107, 101,
37+
],
38+
),
39+
// "Manchester City 의 리그명을 말해줘"
40+
(
41+
"Manchester City",
42+
&[
43+
2, 105, 2364, 107, 74246, 4085, 18132, 225183, 127500, 18906, 237578, 242332, 106,
44+
107, 105, 4368, 107, 100, 45518, 107, 101,
45+
],
46+
),
47+
// "Bayern Munich 어느 리그?"
48+
(
49+
"Bayern Munich",
50+
&[
51+
2, 105, 2364, 107, 218437, 46566, 107561, 225183, 236881, 106, 107, 105, 4368,
52+
107, 100, 45518, 107, 101,
53+
],
54+
),
55+
];
56+
57+
let model_id = std::env::var("MODEL_ID").context("MODEL_ID env required")?;
58+
eprintln!("[probe] loading {model_id}");
59+
let model = NativeGemma4Model::load(Path::new(&model_id)).context("load")?;
60+
61+
let cfg = GenerateConfig {
62+
max_new_tokens: 24,
63+
stop_on_eos: true,
64+
sampling: None,
65+
};
66+
67+
unsafe {
68+
std::env::set_var("LUMEN_GEMMA4_LOOKUP_SPEC", "0");
69+
}
70+
71+
// Warm
72+
let _ = model.generate(queries[0].1, &cfg).context("warm")?;
73+
74+
for (label, prompt) in queries {
75+
let stats = model.generate(prompt, &cfg).context("gen")?;
76+
println!("=== query={label} ({} tok generated) ===", stats.generated_tokens.len());
77+
for (i, t) in stats.generated_tokens.iter().enumerate() {
78+
print!("{t}");
79+
if i + 1 < stats.generated_tokens.len() {
80+
print!(",");
81+
}
82+
}
83+
println!();
84+
}
85+
Ok(())
86+
}
87+
88+
#[cfg(not(feature = "mlx-native"))]
89+
fn main() -> Result<()> {
90+
Err(anyhow::anyhow!("build with --features mlx-native"))
91+
}

0 commit comments

Comments
 (0)