use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use haem_frames::embedding::{Limits, PROTOCOL, PROTOCOL_WINDOWS, Start, Windows, check_start};
use crate::serve::query::Setting;
use crate::serve::storage::embedded::WINDOWS_MOST;
pub const NAMES: [&str; 14] = [
"embed_worker",
"embed_model_dir",
"embed_model_sha256",
"embed_memory_bytes",
"embed_threads",
"embed_batch_items",
"embed_input_bytes",
"embed_tokens",
"embed_frame_bytes",
"embed_ready_ms",
"embed_call_ms",
"embed_waiting",
"embed_request_items",
"embed_page_records",
];
pub const PAGE_RECORDS_MOST: usize = 1024;
#[derive(Clone, Debug, Default)]
pub struct Options {
pub worker: Setting<PathBuf>,
pub model_dir: Setting<PathBuf>,
pub model_sha256: Setting<String>,
pub memory_bytes: Setting<u64>,
pub threads: Setting<u64>,
pub batch_items: Setting<u64>,
pub input_bytes: Setting<u64>,
pub tokens: Setting<u64>,
pub frame_bytes: Setting<u32>,
pub ready_ms: Setting<u64>,
pub call_ms: Setting<u64>,
pub waiting: Setting<u64>,
pub request_items: Setting<u64>,
pub page_records: Setting<u64>,
pub writer_wait_ms: Setting<u64>,
pub windows: Setting<bool>,
pub max_windows: Setting<u64>,
pub overlap_tokens: Setting<u64>,
}
pub const MAX_WINDOWS: u64 = 32;
pub const OVERLAP_TOKENS: u64 = 64;
#[derive(Clone, Debug)]
pub struct Settings {
pub worker: PathBuf,
pub model_dir: String,
pub model_sha256: String,
pub limits: Limits,
pub batch: NonZeroUsize,
pub ready: Duration,
pub call: Duration,
pub waiting: usize,
pub request_items: NonZeroUsize,
pub page_records: NonZeroUsize,
pub writer_wait: Duration,
pub windows: Option<Windows>,
}
impl Settings {
#[must_use]
pub fn document_call(&self) -> Duration {
let windows = self.windows.map_or(1, |windows| windows.max_windows);
self.call
.saturating_mul(u32::try_from(windows).unwrap_or(u32::MAX))
}
}
fn required<T>(setting: Setting<T>, name: &str) -> Result<T, String> {
match setting {
Setting::Present(value) => Ok(value),
Setting::Absent => Err(format!("missing embed setting {name}")),
}
}
fn count(setting: Setting<u64>, name: &str) -> Result<NonZeroUsize, String> {
let value = required(setting, name)?;
usize::try_from(value)
.ok()
.and_then(NonZeroUsize::new)
.ok_or_else(|| format!("embed setting {name} must be a positive count"))
}
fn page_records(setting: Setting<u64>) -> Result<NonZeroUsize, String> {
let records = count(setting, NAMES[13])?;
if records.get() > PAGE_RECORDS_MOST {
return Err(format!(
"embed setting {} must be from 1 to {PAGE_RECORDS_MOST}",
NAMES[13]
));
}
Ok(records)
}
fn millis(setting: Setting<u64>, name: &str) -> Result<Duration, String> {
let duration = Duration::from_millis(required(setting, name)?);
if duration.is_zero() || Instant::now().checked_add(duration).is_none() {
return Err(format!(
"embed setting {name} must be positive and within the clock's range"
));
}
Ok(duration)
}
impl Options {
pub fn resolve(self) -> Result<Option<Settings>, String> {
let present = [
self.worker.is_set(),
self.model_dir.is_set(),
self.model_sha256.is_set(),
self.memory_bytes.is_set(),
self.threads.is_set(),
self.batch_items.is_set(),
self.input_bytes.is_set(),
self.tokens.is_set(),
self.frame_bytes.is_set(),
self.ready_ms.is_set(),
self.call_ms.is_set(),
self.waiting.is_set(),
self.request_items.is_set(),
self.page_records.is_set(),
];
let windowing = [
self.windows.is_set(),
self.max_windows.is_set(),
self.overlap_tokens.is_set(),
];
if present.iter().chain(&windowing).all(|set| !set) && !self.writer_wait_ms.is_set() {
return Ok(None);
}
let missing: Vec<_> = NAMES
.into_iter()
.zip(present)
.filter_map(|(name, set)| (!set).then_some(name))
.collect();
if !missing.is_empty() {
return Err(format!("missing embed settings: {}", missing.join(", ")));
}
let worker = required(self.worker, NAMES[0])?;
if !worker.is_absolute() {
return Err(format!(
"embed setting {} must be an absolute path",
NAMES[0]
));
}
let model_dir = required(self.model_dir, NAMES[1])?
.into_os_string()
.into_string()
.map_err(|_| format!("embed setting {} must be UTF-8", NAMES[1]))?;
let limits = Limits {
memory_bytes: required(self.memory_bytes, NAMES[3])?,
threads: required(self.threads, NAMES[4])?,
batch_items: required(self.batch_items, NAMES[5])?,
input_bytes: required(self.input_bytes, NAMES[6])?,
tokens: required(self.tokens, NAMES[7])?,
frame_bytes: required(self.frame_bytes, NAMES[8])?,
};
let windows = windows(self.windows, self.max_windows, self.overlap_tokens)?;
let start = Start {
protocol: if windows.is_some() {
PROTOCOL_WINDOWS
} else {
PROTOCOL
},
model_dir,
limits,
windows,
};
check_start(&start).map_err(|error| format!("embed settings: {error}"))?;
Ok(Some(Settings {
worker,
model_dir: start.model_dir,
model_sha256: required(self.model_sha256, NAMES[2])?,
limits,
batch: count(Setting::Present(limits.batch_items), NAMES[5])?,
ready: millis(self.ready_ms, NAMES[9])?,
call: millis(self.call_ms, NAMES[10])?,
waiting: count(self.waiting, NAMES[11])?.get(),
request_items: count(self.request_items, NAMES[12])?,
page_records: page_records(self.page_records)?,
writer_wait: writer_wait(self.writer_wait_ms)?,
windows,
}))
}
}
fn windows(
on: Setting<bool>,
max_windows: Setting<u64>,
overlap_tokens: Setting<u64>,
) -> Result<Option<Windows>, String> {
let given = |setting: Setting<u64>, default: u64| match setting {
Setting::Present(value) => value,
Setting::Absent => default,
};
match on {
Setting::Present(true) => {
let max_windows = given(max_windows, MAX_WINDOWS);
if !(2..=u64::from(WINDOWS_MOST)).contains(&max_windows) {
return Err(format!(
"embed setting embed_max_windows is {max_windows}; it must be 2 to \
{WINDOWS_MOST}"
));
}
Ok(Some(Windows {
overlap_tokens: given(overlap_tokens, OVERLAP_TOKENS),
max_windows,
}))
}
Setting::Present(false) | Setting::Absent
if max_windows.is_set() || overlap_tokens.is_set() =>
{
Err(
"embed settings embed_max_windows and embed_window_overlap_tokens need \
embed_windows true"
.to_owned(),
)
}
Setting::Present(false) | Setting::Absent => Ok(None),
}
}
pub const RESTART: &str = "embed_restart_ms";
pub const RESTART_MOST: &str = "embed_restart_most_ms";
pub const RESTART_DEFAULT_MS: u64 = 1_000;
pub const RESTART_MOST_DEFAULT_MS: u64 = 300_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Restart {
pub first: Duration,
pub most: Duration,
pub ready: Duration,
}
impl Default for Restart {
fn default() -> Self {
Self {
first: Duration::from_millis(RESTART_DEFAULT_MS),
most: Duration::from_millis(RESTART_MOST_DEFAULT_MS),
ready: Duration::ZERO,
}
}
}
impl Restart {
pub fn of(first: Setting<u64>, most: Setting<u64>, ready: Duration) -> Result<Self, String> {
let first = millis(given_or(first, RESTART_DEFAULT_MS), RESTART)?;
let most = millis(given_or(most, RESTART_MOST_DEFAULT_MS), RESTART_MOST)?;
if most < first {
return Err(format!(
"embed setting {RESTART_MOST} must be at least {RESTART}"
));
}
Ok(Self { first, most, ready })
}
#[must_use]
pub fn wait(self, deaths: u32) -> Duration {
let Some(doublings) = deaths.checked_sub(2) else {
return Duration::ZERO;
};
1_u32
.checked_shl(doublings)
.and_then(|times| self.first.checked_mul(times))
.map_or(self.most, |wait| wait.min(self.most))
}
}
const fn given_or(setting: Setting<u64>, default: u64) -> Setting<u64> {
match setting {
Setting::Present(value) => Setting::Present(value),
Setting::Absent => Setting::Present(default),
}
}
pub const WORKERS: &str = "embed_workers";
pub const MAX_WORKERS: usize = 16;
pub fn workers(setting: Setting<u64>) -> Result<NonZeroUsize, String> {
let Setting::Present(value) = setting else {
return Ok(NonZeroUsize::MIN);
};
usize::try_from(value)
.ok()
.filter(|count| *count <= MAX_WORKERS)
.and_then(NonZeroUsize::new)
.ok_or_else(|| format!("embed setting {WORKERS} must be from 1 to {MAX_WORKERS}"))
}
pub const WRITER_WAIT_DEFAULT_MS: u64 = 1_000;
pub fn writer_wait(setting: Setting<u64>) -> Result<Duration, String> {
let millis = match setting {
Setting::Absent => WRITER_WAIT_DEFAULT_MS,
Setting::Present(millis) => millis,
};
let wait = Duration::from_millis(millis);
if wait.is_zero() || Instant::now().checked_add(wait).is_none() {
return Err(
"embed_writer_wait_ms must be positive and within the clock's range".to_owned(),
);
}
Ok(wait)
}
#[cfg(test)]
#[path = "settings_tests.rs"]
mod tests;