Recover the model runtime from panics

This commit is contained in:
Georg Bauer
2026-07-25 13:00:20 +02:00
parent 90400701fa
commit 8f91a06336
2 changed files with 159 additions and 93 deletions

View File

@@ -1651,7 +1651,10 @@ fn encode_batch_layer(
"batch raw prefill attention", "batch raw prefill attention",
)?; )?;
} else if pos == 0 && ratio == 4 && compressed_rows > shape.indexer_top_k as u32 { } else if pos == 0 && ratio == 4 && compressed_rows > shape.indexer_top_k as u32 {
let indexer = state.indexer.as_ref().unwrap(); let indexer = state
.indexer
.as_ref()
.ok_or("ratio-4 layer is missing indexer state")?;
call( call(
unsafe { unsafe {
ds4_gpu_indexer_scores_prefill_tensor( ds4_gpu_indexer_scores_prefill_tensor(
@@ -1683,7 +1686,10 @@ fn encode_batch_layer(
}, },
"batch indexer top-k", "batch indexer top-k",
)?; )?;
let compression = state.compression.as_ref().unwrap(); let compression = state
.compression
.as_ref()
.ok_or("compressed layer is missing attention state")?;
call( call(
unsafe { unsafe {
ds4_gpu_attention_indexed_mixed_batch_heads_tensor( ds4_gpu_attention_indexed_mixed_batch_heads_tensor(
@@ -1712,7 +1718,10 @@ fn encode_batch_layer(
"batch indexed prefill attention", "batch indexed prefill attention",
)?; )?;
} else if pos == 0 { } else if pos == 0 {
let compression = state.compression.as_ref().unwrap(); let compression = state
.compression
.as_ref()
.ok_or("compressed layer is missing attention state")?;
call( call(
unsafe { unsafe {
ds4_gpu_attention_prefill_static_mixed_heads_tensor( ds4_gpu_attention_prefill_static_mixed_heads_tensor(
@@ -1759,7 +1768,10 @@ fn encode_batch_layer(
"batch resumed raw attention", "batch resumed raw attention",
)?; )?;
} else if ratio == 4 && compressed_rows > shape.indexer_top_k as u32 { } else if ratio == 4 && compressed_rows > shape.indexer_top_k as u32 {
let indexer = state.indexer.as_ref().unwrap(); let indexer = state
.indexer
.as_ref()
.ok_or("ratio-4 layer is missing indexer state")?;
call( call(
unsafe { unsafe {
ds4_gpu_indexer_scores_decode_batch_tensor( ds4_gpu_indexer_scores_decode_batch_tensor(
@@ -1792,7 +1804,10 @@ fn encode_batch_layer(
}, },
"batch resumed indexer top-k", "batch resumed indexer top-k",
)?; )?;
let compression = state.compression.as_ref().unwrap(); let compression = state
.compression
.as_ref()
.ok_or("compressed layer is missing attention state")?;
call( call(
unsafe { unsafe {
ds4_gpu_attention_indexed_mixed_batch_heads_tensor( ds4_gpu_attention_indexed_mixed_batch_heads_tensor(
@@ -1821,7 +1836,10 @@ fn encode_batch_layer(
"batch resumed indexed attention", "batch resumed indexed attention",
)?; )?;
} else { } else {
let compression = state.compression.as_ref().unwrap(); let compression = state
.compression
.as_ref()
.ok_or("compressed layer is missing attention state")?;
call( call(
unsafe { unsafe {
ds4_gpu_attention_decode_mixed_batch_heads_tensor( ds4_gpu_attention_decode_mixed_batch_heads_tensor(

View File

@@ -8,6 +8,8 @@ use std::sync::mpsc::{self, Receiver, Sender};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
const RUNTIME_PANIC_ERROR: &str = "The model runtime hit an internal error and was reset.";
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct GenerationService { pub(crate) struct GenerationService {
commands: Sender<Command>, commands: Sender<Command>,
@@ -121,8 +123,48 @@ fn run(commands: Receiver<Command>, metrics: Arc<Metrics>) {
Ok(command) => { Ok(command) => {
let request_started = Instant::now(); let request_started = Instant::now();
let source = command.checkpoint.source(); let source = command.checkpoint.source();
let events = command.events.clone();
metrics.request_started(source); metrics.request_started(source);
idle_timeout = command.idle_timeout; if let Err(error) = catch_runtime_panic(|| {
run_command(
command,
&mut loaded,
&metrics,
request_started,
source,
&mut last_used,
&mut idle_timeout,
);
}) {
metrics.request_failed(request_started.elapsed());
if loaded.take().is_some() {
metrics.unloaded();
}
let _ = events.send(GenerationEvent::Finished(Err(error)));
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
if loaded.is_some() && last_used.elapsed() >= idle_timeout {
loaded = None;
metrics.unloaded();
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
}
#[allow(clippy::too_many_arguments)]
fn run_command(
command: Command,
loaded: &mut Option<(EngineSettings, Generator)>,
metrics: &Arc<Metrics>,
request_started: Instant,
source: WorkSource,
last_used: &mut Instant,
idle_timeout: &mut Duration,
) {
*idle_timeout = command.idle_timeout;
if loaded if loaded
.as_ref() .as_ref()
.is_none_or(|(settings, _)| settings != &command.engine) .is_none_or(|(settings, _)| settings != &command.engine)
@@ -133,7 +175,7 @@ fn run(commands: Receiver<Command>, metrics: Arc<Metrics>) {
let _ = command.events.send(GenerationEvent::Loading); let _ = command.events.send(GenerationEvent::Loading);
metrics.loading(); metrics.loading();
let load_started = Instant::now(); let load_started = Instant::now();
loaded = match Generator::open(&command.engine, Arc::clone(&metrics)) { *loaded = match Generator::open(&command.engine, Arc::clone(metrics)) {
Ok(generator) => { Ok(generator) => {
let summary = generator.summary(); let summary = generator.summary();
metrics.loaded( metrics.loaded(
@@ -152,7 +194,7 @@ fn run(commands: Receiver<Command>, metrics: Arc<Metrics>) {
} }
}; };
} }
if let Some((_, generator)) = &mut loaded { if let Some((_, generator)) = loaded {
let mut prefill_started = None::<(Instant, u32)>; let mut prefill_started = None::<(Instant, u32)>;
let mut emit = |reasoning, content| { let mut emit = |reasoning, content| {
let _ = command let _ = command
@@ -188,15 +230,16 @@ fn run(commands: Receiver<Command>, metrics: Arc<Metrics>) {
&mut emit, &mut emit,
&mut progress, &mut progress,
), ),
CheckpointTarget::Transient(directory) CheckpointTarget::Transient(directory) | CheckpointTarget::OneShot(directory) => {
| CheckpointTarget::OneShot(directory) => generator.generate_transient( generator.generate_transient(
&directory, &directory,
&command.messages, &command.messages,
&command.turn, &command.turn,
&command.cancel, &command.cancel,
&mut emit, &mut emit,
&mut progress, &mut progress,
), )
}
}; };
match &result { match &result {
Ok(output) => metrics.request_finished( Ok(output) => metrics.request_finished(
@@ -211,20 +254,16 @@ fn run(commands: Receiver<Command>, metrics: Arc<Metrics>) {
Err(_) => metrics.request_failed(request_started.elapsed()), Err(_) => metrics.request_failed(request_started.elapsed()),
} }
let _ = command.events.send(GenerationEvent::Finished(result)); let _ = command.events.send(GenerationEvent::Finished(result));
last_used = Instant::now(); *last_used = Instant::now();
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
if loaded.is_some() && last_used.elapsed() >= idle_timeout {
loaded = None;
metrics.unloaded();
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
} }
} }
fn catch_runtime_panic<T>(operation: impl FnOnce() -> T) -> Result<T, String> {
// Generator owns GPU handles and is not unwind-safe; callers discard it on error.
std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation))
.map_err(|_| RUNTIME_PANIC_ERROR.to_owned())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -241,4 +280,13 @@ mod tests {
assert!(cancel.load(Ordering::Relaxed)); assert!(cancel.load(Ordering::Relaxed));
} }
#[test]
fn runtime_panic_recovery_accepts_the_next_operation() {
assert_eq!(
catch_runtime_panic(|| panic!("injected runtime panic")),
Err(RUNTIME_PANIC_ERROR.to_owned())
);
assert_eq!(catch_runtime_panic(|| 42), Ok(42));
}
} }