use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Mutex};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use crate::engine::InferenceEngine;
#[derive(Debug, thiserror::Error)]
pub enum PoolError {
#[error("engine pool is closed")]
Closed,
#[error("engine pool is unexpectedly empty")]
Empty,
#[error("engine pool mutex was poisoned")]
Poisoned,
}
pub struct EnginePool {
idle: Mutex<Vec<InferenceEngine<'static>>>,
sem: Arc<Semaphore>,
size: usize,
}
impl EnginePool {
pub fn new(engines: Vec<InferenceEngine<'static>>) -> Arc<Self> {
let size = engines.len().max(1);
Arc::new(Self {
idle: Mutex::new(engines),
sem: Arc::new(Semaphore::new(size)),
size,
})
}
pub fn size(&self) -> usize {
self.size
}
pub fn set_metrics_all(
&self,
metrics: &Arc<crate::metrics::InferenceMetrics>,
) -> Result<(), PoolError> {
let mut idle = self.idle.lock().map_err(|_| PoolError::Poisoned)?;
for engine in idle.iter_mut() {
engine.set_metrics(Arc::clone(metrics));
}
Ok(())
}
pub async fn acquire(self: &Arc<Self>) -> Result<EngineLease, PoolError> {
let permit = Arc::clone(&self.sem)
.acquire_owned()
.await
.map_err(|_| PoolError::Closed)?;
let engine = {
let mut idle = self.idle.lock().map_err(|_| PoolError::Poisoned)?;
idle.pop().ok_or(PoolError::Empty)?
};
Ok(EngineLease {
engine: ManuallyDrop::new(engine),
pool: Arc::clone(self),
_permit: permit,
})
}
}
impl std::fmt::Debug for EnginePool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let idle_len = self.idle.lock().map(|g| g.len()).ok();
f.debug_struct("EnginePool")
.field("size", &self.size)
.field("available_permits", &self.sem.available_permits())
.field("idle_len", &idle_len)
.finish()
}
}
pub struct EngineLease {
engine: ManuallyDrop<InferenceEngine<'static>>,
pool: Arc<EnginePool>,
_permit: OwnedSemaphorePermit,
}
impl Deref for EngineLease {
type Target = InferenceEngine<'static>;
fn deref(&self) -> &Self::Target {
&self.engine
}
}
impl DerefMut for EngineLease {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.engine
}
}
impl Drop for EngineLease {
fn drop(&mut self) {
let engine = unsafe { ManuallyDrop::take(&mut self.engine) };
match self.pool.idle.lock() {
Ok(mut idle) => idle.push(engine),
Err(_poisoned) => {
tracing::error!("engine pool mutex poisoned on lease return; dropping replica");
drop(engine);
}
}
}
}
pub fn default_cpu_pool_size() -> usize {
std::thread::available_parallelism()
.map(|n| n.get().min(4))
.unwrap_or(1)
}
pub fn resolve_pool_size(requested: Option<usize>, tier: pictor_kernels::KernelTier) -> usize {
#[cfg(any(feature = "metal", feature = "native-cuda"))]
{
if tier == pictor_kernels::KernelTier::Gpu {
return 1;
}
}
let _ = tier;
requested.unwrap_or_else(default_cpu_pool_size).max(1)
}
pub fn build_pool_from_gguf(
path: impl AsRef<std::path::Path>,
sampling_params: crate::sampling::SamplingParams,
seed: u64,
max_seq_len: usize,
requested_size: Option<usize>,
) -> crate::error::RuntimeResult<(Arc<EnginePool>, pictor_kernels::KernelTier, usize)> {
let (first, gguf) =
InferenceEngine::from_gguf_path_leaked(path, sampling_params.clone(), seed, max_seq_len)?;
let tier = first.kernel_tier();
let size = resolve_pool_size(requested_size, tier);
if requested_size.map(|r| r > size).unwrap_or(false) {
tracing::info!(
requested = requested_size.unwrap_or(0),
effective = size,
tier = %tier,
"engine pool size clamped to 1 on the GPU tier (process-global GPU singleton)"
);
} else {
tracing::info!(size, tier = %tier, "engine pool built");
}
let shared_token_embd = first.model_token_embd();
let mut engines = Vec::with_capacity(size);
engines.push(first);
for _ in 1..size {
let replica = InferenceEngine::from_gguf_static_with_embd(
gguf,
sampling_params.clone(),
seed,
max_seq_len,
Arc::clone(&shared_token_embd),
)?;
engines.push(replica);
}
Ok((EnginePool::new(engines), tier, size))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sampling::SamplingParams;
use pictor_core::config::Qwen3Config;
use std::time::Duration;
fn tiny_engine() -> InferenceEngine<'static> {
InferenceEngine::new(Qwen3Config::tiny_test(), SamplingParams::default(), 42)
}
fn q1_0_g128_data(num_weights: usize) -> Vec<u8> {
let num_blocks = num_weights / 128;
let scale = half::f16::ONE.to_le_bytes();
let mut data = Vec::with_capacity(num_blocks * 18);
for _ in 0..num_blocks {
data.extend_from_slice(&scale);
data.extend_from_slice(&[0xFFu8; 16]);
}
data
}
fn build_tiny_gguf_bytes() -> Vec<u8> {
use pictor_core::gguf::writer::{
GgufWriter, MetadataWriteValue, TensorEntry, TensorType,
};
let h: usize = 128;
let inter: usize = 256;
let num_layers: usize = 2;
let nq: usize = 4;
let nkv: usize = 2;
let hd: usize = 32;
let vocab: usize = 32;
let mut w = GgufWriter::new();
w.add_metadata(
"general.architecture",
MetadataWriteValue::Str("qwen3".into()),
);
w.add_metadata("general.name", MetadataWriteValue::Str("TinyPool".into()));
w.add_metadata("qwen3.embedding_length", MetadataWriteValue::U32(h as u32));
w.add_metadata(
"qwen3.block_count",
MetadataWriteValue::U32(num_layers as u32),
);
w.add_metadata(
"qwen3.attention.head_count",
MetadataWriteValue::U32(nq as u32),
);
w.add_metadata(
"qwen3.attention.head_count_kv",
MetadataWriteValue::U32(nkv as u32),
);
w.add_metadata(
"qwen3.feed_forward_length",
MetadataWriteValue::U32(inter as u32),
);
w.add_metadata("qwen3.vocab_size", MetadataWriteValue::U32(vocab as u32));
w.add_metadata("qwen3.context_length", MetadataWriteValue::U32(512));
w.add_metadata(
"qwen3.attention.layer_norm_rms_epsilon",
MetadataWriteValue::F32(1e-6),
);
w.add_metadata("qwen3.rope.freq_base", MetadataWriteValue::F32(10_000.0));
let f32_ones = |n: usize| -> Vec<u8> {
let mut v = Vec::with_capacity(n * 4);
for _ in 0..n {
v.extend_from_slice(&1.0_f32.to_le_bytes());
}
v
};
w.add_tensor(TensorEntry {
name: "token_embd.weight".into(),
shape: vec![h as u64, vocab as u64],
tensor_type: TensorType::F32,
data: f32_ones(vocab * h),
});
w.add_tensor(TensorEntry {
name: "output_norm.weight".into(),
shape: vec![h as u64],
tensor_type: TensorType::F32,
data: f32_ones(h),
});
w.add_tensor(TensorEntry {
name: "output.weight".into(),
shape: vec![h as u64, vocab as u64],
tensor_type: TensorType::Q1_0G128,
data: q1_0_g128_data(vocab * h),
});
for layer in 0..num_layers {
let pfx = format!("blk.{layer}");
for suffix in ["attn_norm.weight", "ffn_norm.weight"] {
w.add_tensor(TensorEntry {
name: format!("{pfx}.{suffix}"),
shape: vec![h as u64],
tensor_type: TensorType::F32,
data: f32_ones(h),
});
}
for suffix in ["attn_q_norm.weight", "attn_k_norm.weight"] {
w.add_tensor(TensorEntry {
name: format!("{pfx}.{suffix}"),
shape: vec![hd as u64],
tensor_type: TensorType::F32,
data: f32_ones(hd),
});
}
let q1 = |name: &str, shape: Vec<u64>, n: usize| TensorEntry {
name: name.to_string(),
shape,
tensor_type: TensorType::Q1_0G128,
data: q1_0_g128_data(n),
};
w.add_tensor(q1(
&format!("{pfx}.attn_q.weight"),
vec![h as u64, (nq * hd) as u64],
nq * hd * h,
));
w.add_tensor(q1(
&format!("{pfx}.attn_k.weight"),
vec![h as u64, (nkv * hd) as u64],
nkv * hd * h,
));
w.add_tensor(q1(
&format!("{pfx}.attn_v.weight"),
vec![h as u64, (nkv * hd) as u64],
nkv * hd * h,
));
w.add_tensor(q1(
&format!("{pfx}.attn_output.weight"),
vec![(nq * hd) as u64, h as u64],
h * nq * hd,
));
w.add_tensor(q1(
&format!("{pfx}.ffn_gate.weight"),
vec![h as u64, inter as u64],
inter * h,
));
w.add_tensor(q1(
&format!("{pfx}.ffn_up.weight"),
vec![h as u64, inter as u64],
inter * h,
));
w.add_tensor(q1(
&format!("{pfx}.ffn_down.weight"),
vec![inter as u64, h as u64],
h * inter,
));
}
w.to_bytes().expect("GgufWriter::to_bytes")
}
#[test]
fn resolve_pool_size_explicit_cpu() {
let tier = pictor_kernels::KernelTier::Reference;
assert_eq!(resolve_pool_size(Some(8), tier), 8);
assert_eq!(resolve_pool_size(Some(1), tier), 1);
assert_eq!(resolve_pool_size(Some(0), tier), 1);
}
#[test]
fn resolve_pool_size_default_cpu() {
let tier = pictor_kernels::KernelTier::Reference;
assert_eq!(resolve_pool_size(None, tier), default_cpu_pool_size());
}
#[test]
fn default_cpu_pool_size_in_range() {
let n = default_cpu_pool_size();
assert!((1..=4).contains(&n), "expected 1..=4, got {n}");
}
#[cfg(any(feature = "metal", feature = "native-cuda"))]
#[test]
fn resolve_pool_size_gpu_is_clamped_to_one() {
let tier = pictor_kernels::KernelTier::Gpu;
assert_eq!(resolve_pool_size(Some(8), tier), 1);
assert_eq!(resolve_pool_size(None, tier), 1);
assert_eq!(resolve_pool_size(Some(1), tier), 1);
}
#[tokio::test]
async fn pool_size_reflects_input() {
let pool = EnginePool::new(vec![tiny_engine(), tiny_engine()]);
assert_eq!(pool.size(), 2);
}
#[tokio::test]
async fn acquire_blocks_when_exhausted_then_resumes_on_drop() {
let pool = EnginePool::new(vec![tiny_engine(), tiny_engine()]);
let lease_a = pool.acquire().await.expect("acquire a");
let lease_b = pool.acquire().await.expect("acquire b");
assert_eq!(pool.sem.available_permits(), 0);
{
let idle = pool.idle.lock().expect("lock idle");
assert!(idle.is_empty(), "idle should be empty with 2/2 checked out");
}
let pending = pool.acquire();
let timed_out = tokio::time::timeout(Duration::from_millis(150), pending).await;
assert!(
timed_out.is_err(),
"third acquire resolved while pool was exhausted"
);
drop(lease_a);
let lease_c = tokio::time::timeout(Duration::from_millis(500), pool.acquire())
.await
.expect("acquire should resolve after a lease is dropped")
.expect("acquire c");
drop(lease_b);
drop(lease_c);
assert_eq!(pool.sem.available_permits(), 2);
{
let idle = pool.idle.lock().expect("lock idle");
assert_eq!(idle.len(), 2, "all engines should be back in the pool");
}
}
#[tokio::test]
async fn single_element_pool_is_byte_identical_to_direct_engine() {
let config = Qwen3Config::tiny_test();
let params = SamplingParams::default();
let seed = 42u64;
let prompt: Vec<u32> = vec![151644, 872, 9707, 11];
let max_tokens = 8usize;
let mut direct = InferenceEngine::new(config.clone(), params.clone(), seed);
let direct_out = direct
.generate_with_params(&prompt, max_tokens, ¶ms)
.expect("direct generate");
let pool = EnginePool::new(vec![InferenceEngine::new(
config.clone(),
params.clone(),
seed,
)]);
let mut lease = pool.acquire().await.expect("acquire");
let pool_out = lease
.generate_with_params(&prompt, max_tokens, ¶ms)
.expect("pool generate");
assert_eq!(
direct_out, pool_out,
"1-element pool output diverged from direct engine — byte-identity broken"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_leases_match_isolated_baselines() {
use std::sync::Arc as StdArc;
let config = Qwen3Config::tiny_test();
let params = SamplingParams {
temperature: 0.0,
..SamplingParams::default()
};
let seed = 42u64;
let max_tokens = 6usize;
let prompts: Vec<Vec<u32>> = vec![
vec![151644, 872],
vec![151644, 9707, 11, 1879],
vec![151644, 1986, 374, 264, 1273],
vec![151644, 264],
];
let mut baselines = Vec::with_capacity(prompts.len());
for p in &prompts {
let mut e = InferenceEngine::new(config.clone(), params.clone(), seed);
let out = e
.generate_with_params(p, max_tokens, ¶ms)
.expect("baseline generate");
baselines.push(out);
}
let engines: Vec<InferenceEngine<'static>> = (0..prompts.len())
.map(|_| InferenceEngine::new(config.clone(), params.clone(), seed))
.collect();
let pool = EnginePool::new(engines);
let params = StdArc::new(params);
let mut handles = Vec::with_capacity(prompts.len());
for p in prompts.clone() {
let pool = StdArc::clone(&pool);
let params = StdArc::clone(¶ms);
handles.push(tokio::spawn(async move {
let mut lease = pool.acquire().await.expect("acquire");
lease
.generate_with_params(&p, max_tokens, ¶ms)
.expect("concurrent generate")
}));
}
for (i, h) in handles.into_iter().enumerate() {
let got = h.await.expect("task join");
assert_eq!(
got, baselines[i],
"concurrent task {i} diverged from its isolated baseline — KV/RNG cross-talk"
);
}
}
#[tokio::test]
async fn pool_replicas_share_one_token_embd_allocation() {
let bytes = build_tiny_gguf_bytes();
let path = {
let mut p = std::env::temp_dir();
p.push(format!(
"pictor_pool_shared_embd_{}.gguf",
std::process::id()
));
p
};
std::fs::write(&path, &bytes).expect("write temp GGUF");
let (pool, _tier, size) =
build_pool_from_gguf(&path, SamplingParams::default(), 42, 512, Some(3))
.expect("build_pool_from_gguf");
let _ = std::fs::remove_file(&path);
if size <= 1 {
let lease = pool.acquire().await.expect("acquire sole replica");
let embd = lease.model_token_embd();
assert!(!embd.is_empty(), "token_embd must be populated");
return;
}
let mut leases = Vec::with_capacity(size);
for _ in 0..size {
leases.push(pool.acquire().await.expect("acquire replica"));
}
let first_embd = leases[0].model_token_embd();
for (i, lease) in leases.iter().enumerate().skip(1) {
let other = lease.model_token_embd();
assert!(
Arc::ptr_eq(&first_embd, &other),
"replica #{i} token_embd is a different allocation — sharing broken"
);
}
let kv_ptrs: Vec<*const _> = leases
.iter()
.map(|l| l.model().kv_cache() as *const _)
.collect();
for i in 0..kv_ptrs.len() {
for j in (i + 1)..kv_ptrs.len() {
assert_ne!(
kv_ptrs[i], kv_ptrs[j],
"replicas #{i} and #{j} share a KV cache — isolation broken"
);
}
}
assert_eq!(
Arc::strong_count(&first_embd),
size + 1,
"expected {size} replicas + the local handle to alias one allocation"
);
}
}