[autoresearch] baseline: system prompt quality tests (5/5 passing)

This commit is contained in:
evandrodevbr committed 2026-04-11 05:27:36 -03:00
1 parent 05cc036f6d
commit 60b0101717
142 files changed
+18589 -9947

No files matched your search

+46 -18
View File
@@ -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");
}
}
+73 -7
View File
@@ -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 {
+4 -11
View File
@@ -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.