feat: more preferences
This commit is contained in:
364
src/app.rs
364
src/app.rs
@@ -4,7 +4,11 @@ pub(crate) use view::app_theme;
|
||||
|
||||
use crate::database::{AppPreferences, Database, ProjectWithSessions};
|
||||
use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, ModelChoice};
|
||||
use crate::settings::{ExecutionPreferences, GenerationPreferences, ReasoningMode};
|
||||
use crate::settings::{
|
||||
DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences, ReasoningMode,
|
||||
RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
|
||||
StreamingCacheBudget,
|
||||
};
|
||||
use iced::{Size, Subscription, Task, keyboard, window};
|
||||
use rfd::AsyncFileDialog;
|
||||
use std::fs;
|
||||
@@ -35,6 +39,22 @@ struct PreferenceDraft {
|
||||
prefill_chunk: String,
|
||||
quality: bool,
|
||||
warm_weights: bool,
|
||||
mtp_draft_tokens: String,
|
||||
mtp_margin: String,
|
||||
glm_mtp: bool,
|
||||
glm_mtp_timing: bool,
|
||||
dspark_confidence_threshold: String,
|
||||
dspark_strict: bool,
|
||||
ssd_streaming: bool,
|
||||
ssd_streaming_cold: bool,
|
||||
ssd_cache: String,
|
||||
ssd_full_layers: String,
|
||||
ssd_preload_experts: String,
|
||||
directional_steering_file: String,
|
||||
directional_steering_ffn: String,
|
||||
directional_steering_attn: String,
|
||||
simulated_used_memory_gib: String,
|
||||
expert_profile_path: String,
|
||||
}
|
||||
|
||||
impl PreferenceDraft {
|
||||
@@ -42,11 +62,13 @@ impl PreferenceDraft {
|
||||
let model = ModelChoice::from_id(&preferences.selected_model)
|
||||
.ok_or_else(|| format!("Unsupported model: {}", preferences.selected_model))?;
|
||||
let generation = preferences.generation()?;
|
||||
let execution = preferences.execution()?;
|
||||
execution.validate(model)?;
|
||||
let runtime = preferences.runtime()?;
|
||||
runtime.engine_settings(model)?;
|
||||
let execution = &runtime.execution;
|
||||
let speculative = &runtime.speculative;
|
||||
Ok(Self {
|
||||
model,
|
||||
dspark_enabled: model.supports_dspark() && preferences.dspark_enabled,
|
||||
dspark_enabled: speculative.dspark_enabled,
|
||||
idle_timeout_minutes: preferences.idle_timeout_minutes.to_string(),
|
||||
context_tokens: generation.context_tokens.to_string(),
|
||||
max_generated_tokens: generation.max_generated_tokens.to_string(),
|
||||
@@ -75,6 +97,45 @@ impl PreferenceDraft {
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
quality: execution.quality,
|
||||
warm_weights: execution.warm_weights,
|
||||
mtp_draft_tokens: speculative.mtp_draft_tokens.to_string(),
|
||||
mtp_margin: speculative.mtp_margin.to_string(),
|
||||
glm_mtp: speculative.glm_mtp,
|
||||
glm_mtp_timing: speculative.glm_mtp_timing,
|
||||
dspark_confidence_threshold: speculative
|
||||
.dspark_confidence_threshold
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
dspark_strict: speculative.dspark_strict,
|
||||
ssd_streaming: runtime.ssd.enabled,
|
||||
ssd_streaming_cold: runtime.ssd.cold,
|
||||
ssd_cache: runtime
|
||||
.ssd
|
||||
.cache
|
||||
.map_or_else(String::new, |cache| match cache {
|
||||
StreamingCacheBudget::Experts(experts) => experts.to_string(),
|
||||
StreamingCacheBudget::Gib(gib) => format!("{gib}GB"),
|
||||
}),
|
||||
ssd_full_layers: runtime
|
||||
.ssd
|
||||
.full_layers
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
ssd_preload_experts: runtime
|
||||
.ssd
|
||||
.preload_experts
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
directional_steering_file: runtime.steering.file.unwrap_or_default(),
|
||||
directional_steering_ffn: runtime
|
||||
.steering
|
||||
.ffn_scale
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
directional_steering_attn: runtime
|
||||
.steering
|
||||
.attention_scale
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
simulated_used_memory_gib: runtime
|
||||
.diagnostics
|
||||
.simulated_used_memory_gib
|
||||
.map_or_else(String::new, |value| value.to_string()),
|
||||
expert_profile_path: runtime.diagnostics.expert_profile_path.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,6 +160,8 @@ impl PreferenceDraft {
|
||||
fn reset(&mut self) {
|
||||
let defaults = GenerationPreferences::default();
|
||||
let execution = ExecutionPreferences::default();
|
||||
let speculative = SpeculativePreferences::default();
|
||||
let ssd = SsdPreferences::default();
|
||||
self.model = ModelChoice::default();
|
||||
self.dspark_enabled = false;
|
||||
self.idle_timeout_minutes = "10".into();
|
||||
@@ -115,6 +178,22 @@ impl PreferenceDraft {
|
||||
self.prefill_chunk.clear();
|
||||
self.quality = execution.quality;
|
||||
self.warm_weights = execution.warm_weights;
|
||||
self.mtp_draft_tokens = speculative.mtp_draft_tokens.to_string();
|
||||
self.mtp_margin = speculative.mtp_margin.to_string();
|
||||
self.glm_mtp = speculative.glm_mtp;
|
||||
self.glm_mtp_timing = speculative.glm_mtp_timing;
|
||||
self.dspark_confidence_threshold.clear();
|
||||
self.dspark_strict = speculative.dspark_strict;
|
||||
self.ssd_streaming = ssd.enabled;
|
||||
self.ssd_streaming_cold = ssd.cold;
|
||||
self.ssd_cache.clear();
|
||||
self.ssd_full_layers.clear();
|
||||
self.ssd_preload_experts.clear();
|
||||
self.directional_steering_file.clear();
|
||||
self.directional_steering_ffn.clear();
|
||||
self.directional_steering_attn.clear();
|
||||
self.simulated_used_memory_gib.clear();
|
||||
self.expert_profile_path.clear();
|
||||
}
|
||||
|
||||
fn execution(&self) -> Result<ExecutionPreferences, String> {
|
||||
@@ -126,6 +205,56 @@ impl PreferenceDraft {
|
||||
warm_weights: self.warm_weights,
|
||||
})
|
||||
}
|
||||
|
||||
fn speculative(&self) -> Result<SpeculativePreferences, String> {
|
||||
Ok(SpeculativePreferences {
|
||||
mtp_draft_tokens: parse_positive_i32("MTP draft tokens", &self.mtp_draft_tokens)?,
|
||||
mtp_margin: parse_f32("MTP margin", &self.mtp_margin)?,
|
||||
glm_mtp: self.glm_mtp,
|
||||
glm_mtp_timing: self.glm_mtp_timing,
|
||||
dspark_enabled: self.dspark_enabled,
|
||||
dspark_confidence_threshold: parse_optional_f32(
|
||||
"DSpark confidence",
|
||||
&self.dspark_confidence_threshold,
|
||||
)?,
|
||||
dspark_strict: self.dspark_strict,
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime(&self) -> Result<RuntimePreferences, String> {
|
||||
Ok(RuntimePreferences {
|
||||
execution: self.execution()?,
|
||||
speculative: self.speculative()?,
|
||||
ssd: SsdPreferences {
|
||||
enabled: self.ssd_streaming,
|
||||
cold: self.ssd_streaming_cold,
|
||||
cache: parse_streaming_cache(&self.ssd_cache)?,
|
||||
full_layers: parse_optional_u32("SSD full-layer count", &self.ssd_full_layers)?,
|
||||
preload_experts: parse_optional_u32(
|
||||
"SSD preload experts",
|
||||
&self.ssd_preload_experts,
|
||||
)?,
|
||||
},
|
||||
steering: SteeringPreferences {
|
||||
file: optional_text(&self.directional_steering_file),
|
||||
ffn_scale: parse_optional_f32(
|
||||
"Directional FFN scale",
|
||||
&self.directional_steering_ffn,
|
||||
)?,
|
||||
attention_scale: parse_optional_f32(
|
||||
"Directional attention scale",
|
||||
&self.directional_steering_attn,
|
||||
)?,
|
||||
},
|
||||
diagnostics: DiagnosticPreferences {
|
||||
simulated_used_memory_gib: parse_optional_gib(
|
||||
"Simulated used memory",
|
||||
&self.simulated_used_memory_gib,
|
||||
)?,
|
||||
expert_profile_path: optional_text(&self.expert_profile_path),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_positive_i32(name: &str, value: &str) -> Result<i32, String> {
|
||||
@@ -149,6 +278,13 @@ fn parse_optional_f32(name: &str, value: &str) -> Result<Option<f32>, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_f32(name: &str, value: &str) -> Result<f32, String> {
|
||||
value
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| format!("{name} must be a number."))
|
||||
}
|
||||
|
||||
fn parse_optional_u64(name: &str, value: &str) -> Result<Option<u64>, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
@@ -184,6 +320,66 @@ fn parse_optional_number<T: std::str::FromStr>(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_streaming_cache(value: &str) -> Result<Option<StreamingCacheBudget>, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if value.len() > 2
|
||||
&& value
|
||||
.get(value.len() - 2..)
|
||||
.is_some_and(|suffix| suffix.eq_ignore_ascii_case("gb"))
|
||||
{
|
||||
return parse_gib("SSD cache budget", value)
|
||||
.map(|gib| Some(StreamingCacheBudget::Gib(gib)));
|
||||
}
|
||||
if !value.chars().all(|character| character.is_ascii_digit()) {
|
||||
return Err(
|
||||
"SSD cache budget must be a positive expert count or whole GiB value such as 64GB."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
value
|
||||
.parse::<u32>()
|
||||
.ok()
|
||||
.filter(|value| *value > 0)
|
||||
.map(StreamingCacheBudget::Experts)
|
||||
.map(Some)
|
||||
.ok_or_else(|| {
|
||||
"SSD cache budget must be a positive expert count or whole GiB value such as 64GB."
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_optional_gib(name: &str, value: &str) -> Result<Option<u64>, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
parse_gib(name, value).map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_gib(name: &str, value: &str) -> Result<u64, String> {
|
||||
let value = value
|
||||
.get(value.len().saturating_sub(2)..)
|
||||
.filter(|suffix| suffix.eq_ignore_ascii_case("gb"))
|
||||
.map_or(value, |_| &value[..value.len() - 2]);
|
||||
if !value.chars().all(|character| character.is_ascii_digit()) {
|
||||
return Err(format!("{name} must be a positive whole GiB value."));
|
||||
}
|
||||
value
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.filter(|value| *value > 0 && *value <= u64::MAX / GIB)
|
||||
.ok_or_else(|| format!("{name} must be a positive whole GiB value."))
|
||||
}
|
||||
|
||||
fn optional_text(value: &str) -> Option<String> {
|
||||
let value = value.trim();
|
||||
(!value.is_empty()).then(|| value.to_owned())
|
||||
}
|
||||
|
||||
pub(crate) struct App {
|
||||
main_window: window::Id,
|
||||
pub(super) model_manager_window: Option<window::Id>,
|
||||
@@ -258,6 +454,22 @@ pub(crate) enum Message {
|
||||
PreferencePrefillChunkChanged(String),
|
||||
PreferenceQualityChanged(bool),
|
||||
PreferenceWarmWeightsChanged(bool),
|
||||
PreferenceMtpDraftChanged(String),
|
||||
PreferenceMtpMarginChanged(String),
|
||||
PreferenceGlmMtpChanged(bool),
|
||||
PreferenceGlmMtpTimingChanged(bool),
|
||||
PreferenceDsparkConfidenceChanged(String),
|
||||
PreferenceDsparkStrictChanged(bool),
|
||||
PreferenceSsdChanged(bool),
|
||||
PreferenceSsdColdChanged(bool),
|
||||
PreferenceSsdCacheChanged(String),
|
||||
PreferenceSsdFullLayersChanged(String),
|
||||
PreferenceSsdPreloadChanged(String),
|
||||
PreferenceSteeringFileChanged(String),
|
||||
PreferenceSteeringFfnChanged(String),
|
||||
PreferenceSteeringAttnChanged(String),
|
||||
PreferenceSimulatedMemoryChanged(String),
|
||||
PreferenceExpertProfileChanged(String),
|
||||
ResetPreferences,
|
||||
SavePreferences,
|
||||
DownloadArtifact(ManagedArtifactId),
|
||||
@@ -388,16 +600,31 @@ impl App {
|
||||
self.preference_draft.model = model;
|
||||
if !model.supports_dspark() {
|
||||
self.preference_draft.dspark_enabled = false;
|
||||
self.preference_draft.dspark_confidence_threshold.clear();
|
||||
self.preference_draft.dspark_strict = false;
|
||||
}
|
||||
if model == ModelChoice::Glm52 {
|
||||
self.preference_draft.power_percent.clear();
|
||||
self.preference_draft.prefill_chunk.clear();
|
||||
self.preference_draft.directional_steering_file.clear();
|
||||
self.preference_draft.directional_steering_ffn.clear();
|
||||
self.preference_draft.directional_steering_attn.clear();
|
||||
} else {
|
||||
self.preference_draft.glm_mtp = false;
|
||||
self.preference_draft.glm_mtp_timing = false;
|
||||
self.preference_draft.ssd_full_layers.clear();
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceDsparkChanged(enabled) => {
|
||||
self.preference_draft.dspark_enabled =
|
||||
self.preference_draft.model.supports_dspark() && enabled;
|
||||
if !self.preference_draft.dspark_enabled {
|
||||
self.preference_draft.dspark_confidence_threshold.clear();
|
||||
self.preference_draft.dspark_strict = false;
|
||||
} else {
|
||||
self.preference_draft.ssd_streaming = false;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceTimeoutChanged(value) => {
|
||||
@@ -456,6 +683,106 @@ impl App {
|
||||
self.preference_draft.warm_weights = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceMtpDraftChanged(value) => {
|
||||
self.preference_draft.mtp_draft_tokens = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceMtpMarginChanged(value) => {
|
||||
self.preference_draft.mtp_margin = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceGlmMtpChanged(value) => {
|
||||
self.preference_draft.glm_mtp =
|
||||
self.preference_draft.model == ModelChoice::Glm52 && value;
|
||||
if !self.preference_draft.glm_mtp {
|
||||
self.preference_draft.glm_mtp_timing = false;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceGlmMtpTimingChanged(value) => {
|
||||
self.preference_draft.glm_mtp_timing =
|
||||
self.preference_draft.model == ModelChoice::Glm52 && value;
|
||||
if self.preference_draft.glm_mtp_timing {
|
||||
self.preference_draft.glm_mtp = true;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceDsparkConfidenceChanged(value) => {
|
||||
self.preference_draft.dspark_confidence_threshold = value;
|
||||
if self.preference_draft.model.supports_dspark()
|
||||
&& !self
|
||||
.preference_draft
|
||||
.dspark_confidence_threshold
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
self.preference_draft.dspark_enabled = true;
|
||||
self.preference_draft.ssd_streaming = false;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceDsparkStrictChanged(value) => {
|
||||
self.preference_draft.dspark_strict =
|
||||
self.preference_draft.model.supports_dspark() && value;
|
||||
if self.preference_draft.dspark_strict {
|
||||
self.preference_draft.dspark_enabled = true;
|
||||
self.preference_draft.ssd_streaming = false;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSsdChanged(value) => {
|
||||
self.preference_draft.ssd_streaming = value;
|
||||
if value {
|
||||
self.preference_draft.dspark_enabled = false;
|
||||
self.preference_draft.dspark_confidence_threshold.clear();
|
||||
self.preference_draft.dspark_strict = false;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSsdColdChanged(value) => {
|
||||
self.preference_draft.ssd_streaming_cold = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSsdCacheChanged(value) => {
|
||||
self.preference_draft.ssd_cache = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSsdFullLayersChanged(value) => {
|
||||
if self.preference_draft.model == ModelChoice::Glm52 {
|
||||
self.preference_draft.ssd_full_layers = value;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSsdPreloadChanged(value) => {
|
||||
self.preference_draft.ssd_preload_experts = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSteeringFileChanged(value) => {
|
||||
if self.preference_draft.model != ModelChoice::Glm52 {
|
||||
self.preference_draft.directional_steering_file = value;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSteeringFfnChanged(value) => {
|
||||
if self.preference_draft.model != ModelChoice::Glm52 {
|
||||
self.preference_draft.directional_steering_ffn = value;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSteeringAttnChanged(value) => {
|
||||
if self.preference_draft.model != ModelChoice::Glm52 {
|
||||
self.preference_draft.directional_steering_attn = value;
|
||||
}
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceSimulatedMemoryChanged(value) => {
|
||||
self.preference_draft.simulated_used_memory_gib = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::PreferenceExpertProfileChanged(value) => {
|
||||
self.preference_draft.expert_profile_path = value;
|
||||
self.preference_error = None;
|
||||
}
|
||||
Message::ResetPreferences => {
|
||||
self.preference_draft.reset();
|
||||
self.preference_error = None;
|
||||
@@ -696,28 +1023,21 @@ impl App {
|
||||
};
|
||||
|
||||
let model = self.preference_draft.model;
|
||||
let execution = match self.preference_draft.execution() {
|
||||
Ok(execution) => execution,
|
||||
let runtime = match self.preference_draft.runtime() {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(error) = execution.validate(model) {
|
||||
if let Err(error) = runtime.engine_settings(model) {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
}
|
||||
let dspark_enabled = model.supports_dspark() && self.preference_draft.dspark_enabled;
|
||||
let Some(database) = &mut self.database else {
|
||||
return;
|
||||
};
|
||||
match database.update_preferences(
|
||||
model.id(),
|
||||
dspark_enabled,
|
||||
idle_timeout_minutes,
|
||||
&generation,
|
||||
&execution,
|
||||
) {
|
||||
match database.update_preferences(model.id(), idle_timeout_minutes, &generation, &runtime) {
|
||||
Ok(preferences) => {
|
||||
self.preferences = preferences;
|
||||
self.preference_draft = PreferenceDraft::from_saved(&self.preferences)
|
||||
@@ -957,4 +1277,18 @@ mod tests {
|
||||
assert!(!ModelChoice::DeepSeekV4Pro.supports_dspark());
|
||||
assert!(!ModelChoice::Glm52.supports_dspark());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ds4_gib_and_streaming_cache_inputs_are_typed() {
|
||||
assert_eq!(
|
||||
parse_streaming_cache("128").unwrap(),
|
||||
Some(StreamingCacheBudget::Experts(128))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_streaming_cache("64gB").unwrap(),
|
||||
Some(StreamingCacheBudget::Gib(64))
|
||||
);
|
||||
assert!(parse_streaming_cache("1.5GB").is_err());
|
||||
assert_eq!(parse_optional_gib("Memory", "8GB").unwrap(), Some(8));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user