Serve a machine's models from one shared llama-server
A llama.cpp session had its own `llama-server`: two sessions on one model held two copies of it in memory, a model change bought a load only that session benefited from, and the process was a session's to end. A machine's models are now served by one `llama-server` in **router mode** -- no `-m`, a preset file naming models and their flags, a child server per model asked for, and each request routed by its `model` field. So one server per model with that model's own settings is what a machine runs, while this backend has one process, one port and one record per machine to keep track of. The record is the mechanism every other driver already uses, so a restart adopts it; a session records the same pid in its own directory as `Detail::Shared`, and `process::signal` refuses to signal one of those -- which is what keeps stopping, deleting or cleaning up after one session from unloading a model every other session is using. Nothing stops a router on its own. That is deliberate (a loaded model is minutes of disk) and it is why the machines tab now has a card per provider that opens its own screen: how each model is loaded, how many stay in memory, Unload, and Stop. How a model is *loaded* therefore belongs to the model on its machine rather than to a session -- context size, GPU layers, threads, slots, speculative decoding -- written into the preset as llama-server's own argument names. Saving them re-reads that file, which unloads the model; that is the change taking effect, and the dialog says so before you save. What stays a session's is everything that rides on a request, including which tools it offers: the router hosts one set for the machine and the choice is a filter applied here, so it costs no reload (2,181 tokens of prompt with all seven, 698 with none). Verified end to end against the scratch backend and the emulator: two sessions sharing one loaded model with one child process, a second session joining it with a 26ms prefill, a backend restart adopting the router and answering with the prompt cache intact, the same over ssh to this VM, a model's settings reaching the running server, Unload, and Stop leaving every session `exited` with no error line.
This commit is contained in:
1 parent
74cda485e5
commit
8c323fc7a9
19 files changed
+2601
-511
No files matched your search
@@ -56,6 +56,16 @@ pub enum Detail {
|
||||
/// Spoken to over HTTP on a loopback port, which is all it takes to find
|
||||
/// it again -- there is no stream to be partway through.
|
||||
Http { port: u16 },
|
||||
/// The same, for a process this session reaches but does not own: the
|
||||
/// llama.cpp router serving every session on its machine.
|
||||
///
|
||||
/// A variant rather than a flag because of what it forbids. Liveness is
|
||||
/// the identical question -- a session whose router has gone has no model
|
||||
/// -- but ending it is not this session's to ask, and [`signal`] is where
|
||||
/// that is enforced: stopping, deleting or cleaning up after a session
|
||||
/// must not take a model out of memory for every other session on that
|
||||
/// machine.
|
||||
Shared { port: u16 },
|
||||
}
|
||||
|
||||
/// Whether a recorded process is still there.
|
||||
@@ -82,6 +92,15 @@ impl Record {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether this server may end that process.
|
||||
///
|
||||
/// False for the one it shares -- see [`Detail::Shared`]. Liveness is the
|
||||
/// identical question for both, which is why this is separate from it:
|
||||
/// "is it there?" and "is it mine to end?" are asked in different places.
|
||||
pub fn ours(&self) -> bool {
|
||||
!matches!(self.detail, Detail::Shared { .. })
|
||||
}
|
||||
|
||||
pub fn liveness(&self) -> Liveness {
|
||||
match stat_of(self.pid) {
|
||||
// A different start time is a reused pid, so definitely not ours.
|
||||
@@ -256,10 +275,10 @@ pub const STOP_GRACE: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
/// a SIGKILL would cost whatever it had not flushed; SIGKILL after the grace
|
||||
/// period because a session the phone has deleted must not still be running.
|
||||
pub fn stop(record: &Record, grace: std::time::Duration) {
|
||||
if record.liveness() != Liveness::Alive {
|
||||
if !record.ours() || record.liveness() != Liveness::Alive {
|
||||
return;
|
||||
}
|
||||
signal(record.pid, libc::SIGTERM);
|
||||
signal(record, libc::SIGTERM);
|
||||
let record = record.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(grace).await;
|
||||
@@ -285,6 +304,10 @@ pub fn wait_gone(records: &[Record], grace: std::time::Duration) {
|
||||
|
||||
let deadline = std::time::Instant::now() + grace;
|
||||
for record in records {
|
||||
// Never asked to stop, so there is nothing to wait out.
|
||||
if !record.ours() {
|
||||
continue;
|
||||
}
|
||||
while record.liveness() == Liveness::Alive && std::time::Instant::now() < deadline {
|
||||
std::thread::sleep(LOOK);
|
||||
}
|
||||
@@ -303,17 +326,25 @@ fn kill_if_still_there(record: &Record, grace: std::time::Duration) {
|
||||
record.pid,
|
||||
grace
|
||||
);
|
||||
signal(record.pid, libc::SIGKILL);
|
||||
signal(record, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
fn signal(pid: u32, signal: libc::c_int) {
|
||||
/// The one place a session's process is signalled, which is why the refusal to
|
||||
/// signal a shared one lives here rather than at each caller: every path out of
|
||||
/// a session -- stopped, deleted, cleaned up on the way down -- ends in this
|
||||
/// function, and the one that forgot would be a model unloaded under somebody
|
||||
/// else's turn.
|
||||
fn signal(record: &Record, signal: libc::c_int) {
|
||||
if !record.ours() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: `kill` with a positive pid touches only that process, and the pid
|
||||
// came from a record whose start time was just confirmed to match -- so it
|
||||
// is still the process this server started, not a reused number. A failure
|
||||
// (already gone) is nothing to act on.
|
||||
unsafe {
|
||||
libc::kill(pid as libc::pid_t, signal);
|
||||
libc::kill(record.pid as libc::pid_t, signal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,10 +455,12 @@ mod tests {
|
||||
write(dir.path(), &record);
|
||||
assert_eq!(live(dir.path()), Some(record.clone()));
|
||||
|
||||
// And the other shape round trips through the same file.
|
||||
record.detail = Detail::Http { port: 8080 };
|
||||
write(dir.path(), &record);
|
||||
assert_eq!(live(dir.path()), Some(record));
|
||||
// And the other shapes round trip through the same file.
|
||||
for detail in [Detail::Http { port: 8080 }, Detail::Shared { port: 8080 }] {
|
||||
record.detail = detail;
|
||||
write(dir.path(), &record);
|
||||
assert_eq!(live(dir.path()), Some(record.clone()));
|
||||
}
|
||||
|
||||
mark_stopping(dir.path()).expect("mark stopping");
|
||||
assert!(stopping(dir.path()));
|
||||
@@ -463,6 +496,29 @@ mod tests {
|
||||
assert!(stray.is_empty(), "left behind {stray:?}");
|
||||
}
|
||||
|
||||
/// The whole of what [`Detail::Shared`] is for: a session ending must not
|
||||
/// take the machine's llama.cpp router with it.
|
||||
#[tokio::test]
|
||||
async fn a_shared_process_is_not_stopped_with_the_session_that_reached_it() {
|
||||
let mut child = std::process::Command::new("sleep")
|
||||
.arg("30")
|
||||
.spawn()
|
||||
.expect("spawn sleep");
|
||||
let shared = Record::of(child.id(), Detail::Shared { port: 1 }).expect("start time");
|
||||
stop(&shared, std::time::Duration::from_millis(50));
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
assert_eq!(shared.liveness(), Liveness::Alive, "the router was killed");
|
||||
|
||||
// The same process, recorded as one this session owns, does stop.
|
||||
let owned = Record {
|
||||
detail: Detail::Http { port: 1 },
|
||||
..shared
|
||||
};
|
||||
stop(&owned, std::time::Duration::from_millis(50));
|
||||
let _ = child.wait();
|
||||
assert_eq!(owned.liveness(), Liveness::Dead);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dead_or_unreadable_record_is_not_live() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
Reference in new issue
Block a user