[autoresearch] baseline: system prompt quality tests (5/5 passing)
This commit is contained in:
1 parent
05cc036f6d
commit
60b0101717
142 files changed
+18589
-9947
No files matched your search
@@ -157,19 +157,28 @@ impl InferenceEngine {
|
||||
bail!("Model file not found: {}", config.model_path.display());
|
||||
}
|
||||
|
||||
let model_name = config
|
||||
.model_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "<unknown>".into());
|
||||
|
||||
info!(
|
||||
model = %config.model_path.display(),
|
||||
profile = ?config.profile,
|
||||
n_gpu_layers = config.n_gpu_layers,
|
||||
n_ctx = config.n_ctx,
|
||||
n_batch = config.n_batch,
|
||||
n_ubatch = config.n_ubatch,
|
||||
model = %model_name,
|
||||
profile = ?config.profile,
|
||||
gpu_layers = config.n_gpu_layers,
|
||||
n_ctx = config.n_ctx,
|
||||
n_batch = config.n_batch,
|
||||
n_ubatch = config.n_ubatch,
|
||||
flash_attn = config.flash_attn,
|
||||
use_mmap = config.use_mmap,
|
||||
"Loading model via llama-sys"
|
||||
use_mmap = config.use_mmap,
|
||||
"engine_config"
|
||||
);
|
||||
|
||||
let t_total = std::time::Instant::now();
|
||||
|
||||
// 1. Load dynamic library
|
||||
let t0 = std::time::Instant::now();
|
||||
let lib = Arc::new(LlamaLib::load_auto().context("Failed to load llama.dll")?);
|
||||
|
||||
// 2. Backend init
|
||||
@@ -177,7 +186,10 @@ impl InferenceEngine {
|
||||
(lib.backend_load_all)();
|
||||
(lib.backend_init)();
|
||||
}
|
||||
info!("llama backend initialised");
|
||||
info!(
|
||||
elapsed_ms = t0.elapsed().as_millis(),
|
||||
"llama backend initialised"
|
||||
);
|
||||
|
||||
// 3. Model params
|
||||
let mut mparams = unsafe { (lib.model_default_params)() };
|
||||
@@ -188,10 +200,12 @@ impl InferenceEngine {
|
||||
let model_path_c = CString::new(config.model_path.to_str().context("Non-UTF8 model path")?)
|
||||
.context("CString::new failed")?;
|
||||
|
||||
let t_load = std::time::Instant::now();
|
||||
let model_ptr = unsafe { (lib.model_load_from_file)(model_path_c.as_ptr(), mparams) };
|
||||
if model_ptr.is_null() {
|
||||
bail!("llama_model_load_from_file returned NULL – check model path and VRAM");
|
||||
}
|
||||
let load_ms = t_load.elapsed().as_millis();
|
||||
|
||||
// Log model info
|
||||
{
|
||||
@@ -206,11 +220,12 @@ impl InferenceEngine {
|
||||
let is_hybrid = unsafe { (lib.model_is_hybrid)(model_ptr) };
|
||||
let is_recurrent = unsafe { (lib.model_is_recurrent)(model_ptr) };
|
||||
info!(
|
||||
desc = %desc_str,
|
||||
params_b = n_params as f64 / 1e9,
|
||||
desc = %desc_str,
|
||||
params_b = format!("{:.2}", n_params as f64 / 1e9),
|
||||
is_hybrid,
|
||||
is_recurrent,
|
||||
"Model loaded"
|
||||
load_ms,
|
||||
"model_loaded"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,7 +256,11 @@ impl InferenceEngine {
|
||||
}
|
||||
|
||||
let actual_ctx = unsafe { (lib.n_ctx)(ctx_ptr) };
|
||||
info!(n_ctx = actual_ctx, "Context initialised");
|
||||
info!(
|
||||
n_ctx = actual_ctx,
|
||||
elapsed_ms = t_total.elapsed().as_millis(),
|
||||
"context_ready"
|
||||
);
|
||||
|
||||
let ctx = OwnedContext {
|
||||
ptr: ctx_ptr,
|
||||
@@ -260,7 +279,8 @@ impl InferenceEngine {
|
||||
///
|
||||
/// Call once after `load()` before accepting real requests.
|
||||
pub fn warmup(&mut self) -> Result<()> {
|
||||
info!("Running engine warmup (dummy decode)…");
|
||||
let t = std::time::Instant::now();
|
||||
info!("engine_warmup: starting dummy decode to pre-allocate CUDA buffers");
|
||||
|
||||
unsafe {
|
||||
(self.lib.set_warmup)(self.ctx.ptr, true);
|
||||
@@ -271,18 +291,18 @@ impl InferenceEngine {
|
||||
let ret = (self.lib.decode)(self.ctx.ptr, batch);
|
||||
(self.lib.set_warmup)(self.ctx.ptr, false);
|
||||
if ret != 0 {
|
||||
warn!(ret, "Warmup decode returned non-zero (non-fatal)");
|
||||
warn!(ret, "engine_warmup: decode returned non-zero (non-fatal)");
|
||||
}
|
||||
|
||||
let memory = (self.lib.get_memory)(self.ctx.ptr);
|
||||
if memory.is_null() {
|
||||
warn!("llama_get_memory returned NULL during warmup — KV cache not cleared");
|
||||
warn!("engine_warmup: llama_get_memory returned NULL — KV cache not cleared");
|
||||
} else {
|
||||
(self.lib.memory_clear)(memory, false);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Warmup complete");
|
||||
info!(elapsed_ms = t.elapsed().as_millis(), "engine_ready");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -299,6 +319,14 @@ impl InferenceEngine {
|
||||
// `true` forces a full clear including M-RoPE position metadata; `false` only clears
|
||||
// cell occupancy flags and leaves position data intact, which causes the
|
||||
// "inconsistent sequence positions" error on models that use M-RoPE (e.g. Qwen).
|
||||
// Truncate prompt for logging — show first 120 chars to aid debugging without flooding logs.
|
||||
let prompt_preview = if prompt.len() > 120 {
|
||||
format!("{}…", &prompt[..120])
|
||||
} else {
|
||||
prompt.to_owned()
|
||||
};
|
||||
tracing::debug!(prompt_chars = prompt.len(), preview = %prompt_preview, "session.new");
|
||||
|
||||
unsafe {
|
||||
let memory = (self.lib.get_memory)(self.ctx.ptr);
|
||||
if memory.is_null() {
|
||||
@@ -306,7 +334,7 @@ impl InferenceEngine {
|
||||
anyhow::bail!("KV cache clear failed: llama_get_memory returned NULL");
|
||||
} else {
|
||||
(self.lib.memory_clear)(memory, true);
|
||||
tracing::info!("engine.kv_cache_cleared");
|
||||
tracing::debug!("kv_cache_cleared");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,10 @@ pub struct GenerationSession {
|
||||
pub tokens_generated: u32,
|
||||
pub finished: bool,
|
||||
|
||||
// stop detection
|
||||
recent_output_suffix: String,
|
||||
max_stop_sequence_len: usize,
|
||||
|
||||
// timing
|
||||
pub prefill_ms: Option<f64>,
|
||||
gen_start: Option<Instant>,
|
||||
@@ -203,7 +207,13 @@ impl GenerationSession {
|
||||
"session.prefill_complete"
|
||||
);
|
||||
|
||||
// create session state
|
||||
let max_stop_sequence_len = sampler_cfg
|
||||
.stop_sequences
|
||||
.iter()
|
||||
.map(|stop| stop.chars().count())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Self {
|
||||
lib,
|
||||
ctx,
|
||||
@@ -215,6 +225,8 @@ impl GenerationSession {
|
||||
max_tokens: sampler_cfg.max_tokens,
|
||||
tokens_generated: 0,
|
||||
finished: false,
|
||||
recent_output_suffix: String::new(),
|
||||
max_stop_sequence_len,
|
||||
prefill_ms: Some(prefill_ms),
|
||||
gen_start: None,
|
||||
tokens_per_sec: 0.0,
|
||||
@@ -297,11 +309,16 @@ impl GenerationSession {
|
||||
// Detokenize
|
||||
let token_text = self.detokenize_token(new_token);
|
||||
|
||||
// Check stop sequences
|
||||
// (simple check: if generated text so far ends with a stop seq)
|
||||
// For streaming we check per-token — good enough for Phase 2.
|
||||
for stop in &self.stop_sequences {
|
||||
if token_text.trim_end() == stop.as_str() {
|
||||
// Check stop sequences against the accumulated output suffix so
|
||||
// fragmented tokens like "\n" + "System" + ":" still match.
|
||||
if self.max_stop_sequence_len > 0 && !token_text.is_empty() {
|
||||
push_recent_output_suffix(
|
||||
&mut self.recent_output_suffix,
|
||||
&token_text,
|
||||
self.max_stop_sequence_len,
|
||||
);
|
||||
|
||||
if suffix_matches_any_stop(&self.recent_output_suffix, &self.stop_sequences) {
|
||||
self.finished = true;
|
||||
self.finalize_timing();
|
||||
return None;
|
||||
@@ -427,6 +444,23 @@ fn build_prefill_plan(token_count: usize, max_batch_tokens: usize) -> Vec<Prefil
|
||||
plans
|
||||
}
|
||||
|
||||
fn push_recent_output_suffix(current: &mut String, chunk: &str, max_stop_sequence_len: usize) {
|
||||
if max_stop_sequence_len == 0 || chunk.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
current.push_str(chunk);
|
||||
let suffix_chars = current.chars().count();
|
||||
let keep_chars = max_stop_sequence_len.saturating_mul(2).max(32);
|
||||
if suffix_chars > keep_chars {
|
||||
*current = current.chars().skip(suffix_chars - keep_chars).collect();
|
||||
}
|
||||
}
|
||||
|
||||
fn suffix_matches_any_stop(suffix: &str, stops: &[String]) -> bool {
|
||||
stops.iter().any(|stop| suffix.ends_with(stop))
|
||||
}
|
||||
|
||||
fn decode_prefill(
|
||||
lib: &LlamaLib,
|
||||
ctx: *mut llama_context,
|
||||
@@ -494,7 +528,10 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_prefill_plan, decode_token_piece_with, PrefillChunkPlan};
|
||||
use super::{
|
||||
build_prefill_plan, decode_token_piece_with, push_recent_output_suffix,
|
||||
suffix_matches_any_stop, PrefillChunkPlan,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn retries_when_piece_buffer_is_too_small() {
|
||||
@@ -613,6 +650,35 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_detection_matches_fragmented_suffix_across_multiple_chunks() {
|
||||
let stops = vec!["\nSystem:".to_owned()];
|
||||
let mut suffix = String::new();
|
||||
let max_len = stops[0].chars().count();
|
||||
|
||||
push_recent_output_suffix(&mut suffix, "Resposta util", max_len);
|
||||
assert!(!suffix_matches_any_stop(&suffix, &stops));
|
||||
|
||||
push_recent_output_suffix(&mut suffix, "\n", max_len);
|
||||
assert!(!suffix_matches_any_stop(&suffix, &stops));
|
||||
|
||||
push_recent_output_suffix(&mut suffix, "System", max_len);
|
||||
assert!(!suffix_matches_any_stop(&suffix, &stops));
|
||||
|
||||
push_recent_output_suffix(&mut suffix, ":", max_len);
|
||||
assert!(suffix_matches_any_stop(&suffix, &stops));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix_buffer_is_trimmed_to_a_bounded_window() {
|
||||
let mut suffix = String::new();
|
||||
let max_len = "\nAssistant:".chars().count();
|
||||
|
||||
push_recent_output_suffix(&mut suffix, &"x".repeat(128), max_len);
|
||||
|
||||
assert!(suffix.chars().count() <= max_len.saturating_mul(2).max(32));
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GenerationSession {
|
||||
|
||||
@@ -236,13 +236,6 @@ impl LlamaLib {
|
||||
let lib = unsafe { Library::new(path) }
|
||||
.with_context(|| format!("Failed to load llama library from {:?}", path))?;
|
||||
|
||||
macro_rules! sym {
|
||||
($name:literal) => {{
|
||||
*unsafe { lib.get::<unsafe extern "C" fn()>(concat!($name, "\0").as_bytes()) }
|
||||
.with_context(|| format!("Missing symbol: {}", $name))?
|
||||
}};
|
||||
}
|
||||
|
||||
// We need transmutes to specific fn types — use a helper macro:
|
||||
macro_rules! load {
|
||||
($field:ident, $ty:ty, $name:literal) => {{
|
||||
@@ -556,11 +549,11 @@ impl LlamaLib {
|
||||
return Ok(PathBuf::from(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"Cannot find llama shared library. Set LLAMA_DLL_PATH to the full path of llama.dll"
|
||||
)
|
||||
anyhow::bail!(
|
||||
"Cannot find llama shared library. Set LLAMA_DLL_PATH to the full path of llama.dll"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: find + load in one call.
|
||||
|
||||
Reference in new issue
Block a user