#[cfg(feature = "gpu")]
fn try_gpu_completions(
state: &AppState,
request: &CompletionRequest,
max_tokens: usize,
temperature: f32,
start: std::time::Instant,
cancel: &CancelToken,
) -> Result<Option<CompletionResponse>, RErr> {
use crate::gpu::GpuGenerateConfig;
let gpu_model_lock = match state.gpu_model() {
Some(l) => l,
None => return Ok(None),
};
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(
state,
StatusCode::INTERNAL_SERVER_ERROR,
"No tokenizer available",
)
})?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(
state,
StatusCode::BAD_REQUEST,
"Prompt cannot be empty",
));
}
let prompt_tokens = prompt_ids.len();
let prompt: Vec<usize> = prompt_ids.iter().map(|&id| id as usize).collect();
let gpu_config = GpuGenerateConfig {
max_tokens,
temperature,
top_k: 1,
stop_tokens: Vec::new(),
trace: state.is_trace_enabled(),
cancel: cancel.clone(),
};
let mut gpu_model = gpu_model_lock.write().map_err(|e| {
rerr(
state,
StatusCode::INTERNAL_SERVER_ERROR,
format!("GPU lock: {e}"),
)
})?;
let generated = gpu_model
.generate(&prompt, &gpu_config)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
let token_ids: Vec<u32> = generated
.iter()
.skip(prompt_tokens)
.filter_map(|&id| u32::try_from(id).ok())
.collect();
let completion_tokens = token_ids.len();
let text = tokenizer
.decode(&token_ids)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
let (text, finish_reason) =
apply_stop_sequences(text, request.stop.as_deref(), completion_tokens, max_tokens);
let finish_reason = finish_reason.as_str();
state
.metrics
.record_success(completion_tokens, start.elapsed());
let response_id = format!("cmpl-{}", &uuid::Uuid::new_v4().to_string()[..8]);
Ok(Some(CompletionResponse {
id: response_id,
object: "text_completion".to_string(),
created: epoch_secs(),
model: request.model.clone(),
choices: vec![CompletionChoice {
text,
index: 0,
logprobs: None,
finish_reason: finish_reason.to_string(),
}],
usage: Usage {
prompt_tokens,
completion_tokens,
total_tokens: prompt_tokens + completion_tokens,
},
}))
}
fn try_apr_transformer_completions(
state: &AppState,
request: &CompletionRequest,
max_tokens: usize,
temperature: f32,
start: std::time::Instant,
cancel: &CancelToken,
) -> Result<Option<CompletionResponse>, RErr> {
use crate::apr_transformer::GenerateConfig;
let apr_transformer = match state.apr_transformer() {
Some(m) => m,
None => return Ok(None),
};
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(
state,
StatusCode::INTERNAL_SERVER_ERROR,
"No tokenizer available",
)
})?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(
state,
StatusCode::BAD_REQUEST,
"Prompt cannot be empty",
));
}
let prompt_tokens = prompt_ids.len();
let gen_config = GenerateConfig {
max_tokens,
temperature,
cancel: cancel.clone(),
..Default::default()
};
let generated = apr_transformer
.generate_with_cache(&prompt_ids, &gen_config)
.map_err(|e| {
rerr(
state,
super::generation_error_status(&e),
format!("APR generation failed: {e}"),
)
})?;
let token_ids: Vec<u32> = generated.iter().skip(prompt_tokens).copied().collect();
let completion_tokens = token_ids.len();
let text = tokenizer
.decode(&token_ids)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
state
.metrics
.record_success(completion_tokens, start.elapsed());
Ok(Some(completion_resp(
"cmpl",
request.model.clone(),
text,
prompt_tokens,
completion_tokens,
max_tokens,
request.stop.as_deref(),
)))
}
fn registry_completions(
state: &AppState,
request: &CompletionRequest,
max_tokens: usize,
temperature: f32,
start: std::time::Instant,
cancel: &CancelToken,
) -> Result<CompletionResponse, RErr> {
let model_id = if request.model == "default" || request.model.is_empty() {
None
} else {
Some(request.model.as_str())
};
let (model, tokenizer) = state
.get_model(model_id)
.map_err(|e| rerr(state, super::model_resolution_status(&e), e))?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(
state,
StatusCode::BAD_REQUEST,
"Prompt cannot be empty",
));
}
let prompt_tokens = prompt_ids.len();
let prompt: Vec<usize> = prompt_ids.iter().map(|&id| id as usize).collect();
let config = resolve_dense_generation_config(
temperature,
request.top_p.map(|p| p as f32),
max_tokens,
)
.with_cancel(cancel.clone());
let generated = model
.generate(&prompt, &config)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
let token_ids: Vec<u32> = generated
.iter()
.skip(prompt_tokens)
.filter_map(|&id| u32::try_from(id).ok())
.collect();
let completion_tokens = token_ids.len();
let text = tokenizer
.decode(&token_ids)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
state
.metrics
.record_success(completion_tokens, start.elapsed());
Ok(completion_resp(
"cmpl",
request.model.clone(),
text,
prompt_tokens,
completion_tokens,
max_tokens,
request.stop.as_deref(),
))
}
#[cfg(feature = "cuda")]
async fn try_apr_q4k_completions(
state: &AppState,
request: &CompletionRequest,
max_tokens: usize,
temperature: f32,
start: std::time::Instant,
cancel: &CancelToken,
) -> Result<Option<CompletionResponse>, RErr> {
use crate::api::apr_q4k_scheduler::AprQ4kRequest;
let q4k_tx = match state.apr_q4k_tx() {
Some(tx) => tx,
None => return Ok(None),
};
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(state, StatusCode::INTERNAL_SERVER_ERROR, "No tokenizer available")
})?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(state, StatusCode::BAD_REQUEST, "Prompt cannot be empty"));
}
let prompt_tokens = prompt_ids.len();
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
let eos_ids = state.model_eos_ids();
q4k_tx
.send(AprQ4kRequest {
prompt_ids,
max_tokens,
temperature,
eos_ids,
cancel: cancel.clone(),
response_tx,
})
.await
.map_err(|_| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, "Q4K thread unavailable"))?;
let result = response_rx
.await
.map_err(|_| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, "Q4K thread dropped response"))?;
let resp = result.map_err(|e| {
rerr(state, StatusCode::INTERNAL_SERVER_ERROR, format!("Q4K generation failed: {e}"))
})?;
let text = tokenizer
.decode(&resp.output_tokens)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
let completion_tokens = resp.tokens_generated;
state.metrics.record_success(completion_tokens, start.elapsed());
Ok(Some(completion_resp(
"cmpl",
request.model.clone(),
text,
prompt_tokens,
completion_tokens,
max_tokens,
request.stop.as_deref(),
)))
}
#[cfg(feature = "cuda")]
async fn try_cuda_gguf_completions(
state: &AppState,
request: &CompletionRequest,
max_tokens: usize,
temperature: f32,
start: std::time::Instant,
) -> Result<Option<CompletionResponse>, RErr> {
use crate::api::cuda_batch_scheduler::CudaBatchRequest;
use crate::gguf::QuantizedGenerateConfig;
let batch_tx = match state.cuda_batch_tx() {
Some(tx) => tx,
None => return Ok(None),
};
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(state, StatusCode::INTERNAL_SERVER_ERROR, "No tokenizer available")
})?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(state, StatusCode::BAD_REQUEST, "Prompt cannot be empty"));
}
let prompt_tokens = prompt_ids.len();
let eos = state.cached_eos_token_id.unwrap_or(151643);
let q_config = QuantizedGenerateConfig {
max_tokens,
temperature,
stop_tokens: vec![eos],
..Default::default()
};
let (token_tx, mut token_rx) = tokio::sync::mpsc::channel::<Result<u32, String>>(max_tokens + 1);
let batch_req = CudaBatchRequest {
prompt_ids,
config: q_config,
token_tx,
non_streaming: true,
enqueue_time: std::time::Instant::now(),
};
batch_tx
.try_send(batch_req)
.map_err(|_| rerr(state, StatusCode::SERVICE_UNAVAILABLE, "CUDA batch queue full"))?;
let mut output_tokens = Vec::with_capacity(max_tokens);
while let Some(result) = token_rx.recv().await {
match result {
Ok(token_id) => output_tokens.push(token_id),
Err(e) => {
return Err(rerr(state, StatusCode::INTERNAL_SERVER_ERROR, format!("CUDA generation: {e}")));
}
}
}
let completion_tokens = output_tokens.len();
let text = tokenizer
.decode(&output_tokens)
.map_err(|e| rerr(state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
state.metrics.record_success(completion_tokens, start.elapsed());
Ok(Some(completion_resp(
"cmpl",
request.model.clone(),
text,
prompt_tokens,
completion_tokens,
max_tokens,
request.stop.as_deref(),
)))
}
pub(crate) fn completion_sse_response(response: &CompletionResponse) -> axum::response::Response {
use axum::response::sse::{Event, Sse};
use axum::response::IntoResponse;
let choice = response.choices.first();
let text = choice.map(|c| c.text.as_str()).unwrap_or_default();
let finish_reason = choice.map_or_else(
|| FinishReason::Stop.as_str().to_string(),
|c| c.finish_reason.clone(),
);
let envelope = |text: String, finish_reason: Option<String>| CompletionChunk {
id: response.id.clone(),
object: response.object.clone(),
created: response.created,
model: response.model.clone(),
choices: vec![CompletionChunkChoice {
text,
index: 0,
logprobs: None,
finish_reason,
}],
};
let mut chunks: Vec<CompletionChunk> = crate::api::ollama_handlers::content_fragments(text)
.into_iter()
.map(|fragment| envelope(fragment, None))
.collect();
chunks.push(envelope(String::new(), Some(finish_reason)));
let stream = tokio_stream::iter(
chunks
.into_iter()
.filter_map(|chunk| serde_json::to_string(&chunk).ok())
.chain(std::iter::once("[DONE]".to_string()))
.map(|data| Ok::<Event, std::convert::Infallible>(Event::default().data(data))),
);
Sse::new(stream).into_response()
}
pub async fn openai_completions_handler(
State(state): State<AppState>,
Extension(cancel): Extension<CancelToken>,
Json(request): Json<CompletionRequest>,
) -> Result<axum::response::Response, RErr> {
use axum::response::IntoResponse;
let stream = request.stream;
let completion = completions_inner(state, request, cancel).await?;
Ok(if stream {
completion_sse_response(&completion)
} else {
Json(completion).into_response()
})
}
async fn completions_inner(
state: AppState,
request: CompletionRequest,
cancel: CancelToken,
) -> Result<CompletionResponse, RErr> {
let start = std::time::Instant::now();
let max_tokens = request.max_tokens.unwrap_or(256);
let temperature = request.temperature.unwrap_or(0.7) as f32;
#[cfg(feature = "gpu")]
if let Some(r) =
try_cached_completions(&state, &request, max_tokens, temperature, start, &cancel).await?
{
return Ok(r);
}
if let Some(r) =
try_quantized_completions(&state, &request, max_tokens, temperature, start, &cancel)?
{
return Ok(r);
}
#[cfg(feature = "gpu")]
if let Some(r) =
try_gpu_completions(&state, &request, max_tokens, temperature, start, &cancel)?
{
return Ok(r);
}
#[cfg(feature = "cuda")]
if let Some(r) =
try_apr_q4k_completions(&state, &request, max_tokens, temperature, start, &cancel).await?
{
return Ok(r);
}
#[cfg(feature = "cuda")]
if let Some(r) = try_cuda_gguf_completions(&state, &request, max_tokens, temperature, start).await? {
return Ok(r);
}
#[cfg(feature = "cuda")]
if let Some(cuda_lock) = state.cuda_model() {
use crate::gguf::QuantizedGenerateConfig;
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(&state, StatusCode::INTERNAL_SERVER_ERROR, "No tokenizer")
})?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(&state, StatusCode::BAD_REQUEST, "Prompt cannot be empty"));
}
let eos = state.cached_eos_token_id.unwrap_or(151643);
let config = QuantizedGenerateConfig {
max_tokens: max_tokens.min(4096),
temperature,
top_k: if temperature == 0.0 { 1 } else { 40 },
stop_tokens: vec![eos],
..Default::default()
};
let result = {
let mut model = cuda_lock.write().expect("CUDA model lock");
model.generate_gpu_resident_logprobs(
&prompt_ids.iter().map(|&id| id as u32).collect::<Vec<_>>(),
&config,
).map_err(|e| rerr(&state, StatusCode::INTERNAL_SERVER_ERROR, e))?
};
let prompt_len = prompt_ids.len();
let gen_tokens: Vec<u32> = result.tokens[prompt_len..].to_vec();
let text: String = gen_tokens.iter()
.map(|&t| tokenizer.decode(&[t]).unwrap_or_else(|_| format!("<{t}>")))
.collect();
let elapsed = start.elapsed();
let completion_tokens = gen_tokens.len();
let (text, finish_reason) =
apply_stop_sequences(text, request.stop.as_deref(), completion_tokens, max_tokens);
return Ok(CompletionResponse {
id: format!("cmpl-cuda-{}", elapsed.as_millis()),
object: "text_completion".to_string(),
created: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
model: request.model.clone(),
choices: vec![CompletionChoice {
text,
index: 0,
logprobs: None,
finish_reason: finish_reason.as_str().to_string(),
}],
usage: Usage {
prompt_tokens: prompt_ids.len(),
completion_tokens,
total_tokens: prompt_ids.len() + completion_tokens,
},
});
}
if let Some(r) =
try_apr_transformer_completions(&state, &request, max_tokens, temperature, start, &cancel)?
{
return Ok(r);
}
registry_completions(&state, &request, max_tokens, temperature, start, &cancel)
}
#[cfg(feature = "cuda")]
pub async fn logprobs_handler(
State(state): State<AppState>,
Json(request): Json<CompletionRequest>,
) -> Result<Json<serde_json::Value>, RErr> {
use crate::gguf::QuantizedGenerateConfig;
let cuda_model_lock = state.cuda_model().ok_or_else(|| {
rerr(&state, StatusCode::SERVICE_UNAVAILABLE, "No CUDA model loaded")
})?;
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(&state, StatusCode::INTERNAL_SERVER_ERROR, "No tokenizer")
})?;
let prompt_ids = tokenizer.encode(&request.prompt);
if prompt_ids.is_empty() {
return Err(rerr(&state, StatusCode::BAD_REQUEST, "Empty prompt"));
}
let max_tokens = request.max_tokens.unwrap_or(256);
let eos = state.cached_eos_token_id.unwrap_or(151643);
let config = QuantizedGenerateConfig {
max_tokens,
temperature: 0.0, top_k: 1,
stop_tokens: vec![eos],
logprobs: true,
..Default::default()
};
let result = {
let mut model = cuda_model_lock.write().expect("CUDA model lock");
model.generate_gpu_resident_logprobs(
&prompt_ids.iter().map(|&x| x as u32).collect::<Vec<_>>(),
&config,
).map_err(|e| rerr(&state, StatusCode::INTERNAL_SERVER_ERROR, e))?
};
let prompt_len = prompt_ids.len();
let gen_tokens: Vec<u32> = result.tokens[prompt_len..].to_vec();
let gen_text: Vec<String> = gen_tokens.iter().map(|&t| {
tokenizer.decode(&[t]).unwrap_or_else(|_| format!("<{t}>"))
}).collect();
let n = result.logprobs.len() as f64;
let sum_logprob: f64 = result.logprobs.iter().map(|lp| f64::from(lp.logprob)).sum();
let perplexity = if n > 0.0 { (-sum_logprob / n).exp() } else { 0.0 };
let logprobs_json: Vec<serde_json::Value> = result.logprobs.iter().zip(gen_text.iter()).map(|(lp, text)| {
serde_json::json!({
"token": text,
"token_id": lp.token_id,
"logprob": lp.logprob,
})
}).collect();
Ok(Json(serde_json::json!({
"prompt_tokens": prompt_len,
"completion_tokens": gen_tokens.len(),
"tokens": gen_text,
"logprobs": logprobs_json,
"perplexity": perplexity,
"sum_logprob": sum_logprob,
})))
}
#[cfg(feature = "cuda")]
pub async fn perplexity_handler(
State(state): State<AppState>,
Json(request): Json<CompletionRequest>,
) -> Result<Json<serde_json::Value>, RErr> {
let cuda_model_lock = state.cuda_model().ok_or_else(|| {
rerr(&state, StatusCode::SERVICE_UNAVAILABLE, "No CUDA model loaded")
})?;
let tokenizer = state.tokenizer.clone().ok_or_else(|| {
rerr(&state, StatusCode::INTERNAL_SERVER_ERROR, "No tokenizer")
})?;
let token_ids: Vec<u32> = tokenizer
.encode(&request.prompt)
.iter()
.map(|&x| x as u32)
.collect();
if token_ids.len() < 2 {
return Err(rerr(&state, StatusCode::BAD_REQUEST, "Need at least 2 tokens"));
}
let start = std::time::Instant::now();
let mut model = cuda_model_lock.write().expect("CUDA model lock");
let ppl_sequential = model
.perplexity_gpu_resident(&token_ids)
.map_err(|e| rerr(&state, StatusCode::INTERNAL_SERVER_ERROR, e))?;
let ppl_batched = model.perplexity_gpu_batched(&token_ids).ok();
drop(model);
let elapsed = start.elapsed();
Ok(Json(serde_json::json!({
"perplexity": ppl_batched.unwrap_or(ppl_sequential),
"ppl_sequential": ppl_sequential,
"ppl_batched": ppl_batched,
"num_tokens": token_ids.len(),
"elapsed_ms": elapsed.as_millis(),
"tokens_per_sec": token_ids.len() as f64 / elapsed.as_secs_f64(),
})))
}
pub async fn openai_embeddings_handler(
State(state): State<AppState>,
Json(request): Json<EmbeddingRequest>,
) -> Result<Json<EmbeddingResponse>, (StatusCode, Json<ErrorResponse>)> {
crate::api::realize_handlers::embed_for_route(state, request, "/v1/embeddings")
}
#[cfg(test)]
mod pmat795_finish_reason_tests {
use super::apply_stop_sequences;
fn reason(text: &str, stops: Option<&[String]>, completion_tokens: usize, max: usize) -> String {
apply_stop_sequences(text.to_string(), stops, completion_tokens, max)
.1
.as_str()
.to_string()
}
#[test]
fn max_tokens_hit_with_no_stop_is_length() {
assert_eq!(reason("abc", None, 256, 256), "length");
assert_eq!(reason("abc", None, 300, 256), "length");
}
#[test]
fn natural_termination_before_budget_is_stop() {
assert_eq!(reason("abc", None, 10, 256), "stop");
}
#[test]
fn stop_string_match_beats_length() {
let stops = vec!["X".to_string()];
assert_eq!(reason("abXc", Some(&stops), 256, 256), "stop");
assert_eq!(reason("abXc", Some(&stops), 10, 256), "stop");
}
#[test]
fn unmatched_stop_does_not_beat_length() {
let stops = vec!["ZZZ".to_string()];
assert_eq!(reason("abXc", Some(&stops), 256, 256), "length");
}
}