use axum::{
body::Body,
http::{Request, StatusCode},
};
use serde_json::Value;
use tower::util::ServiceExt;
use super::native_routes_2376::{body_string, post};
use crate::api::{create_router, AppState};
const CHAT: &str = "/v1/chat/completions";
fn tinyllama_path() -> Option<std::path::PathBuf> {
let home = std::env::var_os("HOME")?;
let path = std::path::Path::new(&home)
.join(".apr/models/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf");
path.exists().then_some(path)
}
fn cuda_state() -> Option<AppState> {
use crate::gguf::{MappedGGUFModel, OwnedQuantizedModel, OwnedQuantizedModelCuda};
let Some(path) = tinyllama_path() else {
eprintln!("SKIP: tinyllama not present");
return None;
};
let mapped = MappedGGUFModel::from_path(path).expect("map tinyllama");
let vocab = mapped
.model
.vocabulary()
.expect("tinyllama has a vocabulary");
let model = OwnedQuantizedModel::from_mapped(&mapped).expect("load tinyllama");
let cuda = match OwnedQuantizedModelCuda::new(model, 0) {
Ok(cuda) => cuda,
Err(e) => {
eprintln!("SKIP: no CUDA device: {e}");
return None;
},
};
Some(AppState::with_cuda_model_and_vocab(cuda, vocab).expect("CUDA state"))
}
fn scheduled(state: &AppState) -> AppState {
use crate::api::cuda_batch_scheduler::{spawn_cuda_batch_scheduler, CudaBatchConfig};
let model = state.cuda_model().expect("a CUDA state").clone();
let tx = spawn_cuda_batch_scheduler(
model,
CudaBatchConfig::default(),
crate::api::InFlightCounter::new(),
);
state.clone().with_cuda_batch_tx(tx)
}
fn batch_of_two(state: &AppState) -> (AppState, std::sync::Arc<crate::api::InFlightCounter>) {
use crate::api::cuda_batch_scheduler::{spawn_cuda_batch_scheduler, CudaBatchConfig};
let model = state.cuda_model().expect("a CUDA state").clone();
let counter = crate::api::InFlightCounter::new();
let config = CudaBatchConfig {
max_batch: 2,
window_ms: 10_000,
};
let tx = spawn_cuda_batch_scheduler(model, config, counter.clone());
(state.clone().with_cuda_batch_tx(tx), counter)
}
const WITH: &str = r#","logprobs":true,"top_logprobs":3"#;
fn chat_body(extra: &str) -> String {
format!(
r#"{{"model":"default","messages":[{{"role":"user","content":"The capital of France is"}}],"max_tokens":8,"temperature":0{extra}}}"#
)
}
fn logprob(entry: &Value) -> f64 {
entry["logprob"].as_f64().expect("logprob is a number")
}
async fn chat_json(state: &AppState, extra: &str) -> Value {
let (status, body) = post(state.clone(), CHAT, &chat_body(extra)).await;
assert_eq!(status, StatusCode::OK, "{body}");
serde_json::from_str(&body).expect("chat reply is JSON")
}
async fn streamed_entries(state: &AppState, extra: &str) -> (Vec<Value>, u64) {
let body = chat_body(&format!(r#","stream":true{extra}"#));
let (status, body) = post(state.clone(), CHAT, &body).await;
assert_eq!(status, StatusCode::OK, "{body}");
let chunks: Vec<Value> = body
.lines()
.filter_map(|line| line.strip_prefix("data: "))
.filter(|data| *data != "[DONE]")
.map(|data| serde_json::from_str(data).expect("an SSE chunk is JSON"))
.collect();
assert!(
chunks.iter().all(|c| c.get("error").is_none()),
"the stream carried an error: {body}"
);
let completion_tokens = chunks
.iter()
.find_map(|c| c["usage"]["completion_tokens"].as_u64())
.expect("the terminal chunk carries usage");
let entries = chunks
.iter()
.filter_map(|c| c["choices"][0]["logprobs"]["content"].as_array())
.flatten()
.cloned()
.collect();
(entries, completion_tokens)
}
fn checked_entries<'a>(reply: &'a Value, path: &str) -> &'a [Value] {
let completion_tokens = reply["usage"]["completion_tokens"]
.as_u64()
.expect("usage.completion_tokens");
let content = reply["choices"][0]["logprobs"]["content"]
.as_array()
.unwrap_or_else(|| panic!("{path}: no choices[0].logprobs.content: {reply}"));
assert!(completion_tokens > 0, "{path}: generated nothing: {reply}");
assert_eq!(content.len() as u64, completion_tokens, "{path}: {reply}");
for entry in content {
let tops = entry["top_logprobs"].as_array().expect("top_logprobs");
assert_eq!(tops.len(), 3, "{path}: {entry}");
assert!(logprob(entry) <= 0.0, "{path}: {entry}");
assert!(
tops.windows(2).all(|w| logprob(&w[0]) >= logprob(&w[1])),
"{path}: {entry}"
);
assert!(
(logprob(entry) - logprob(&tops[0])).abs() < 1e-4,
"{path}: {entry}"
);
}
content
}
fn same_entries(a: &[Value], b: &[Value], path: &str) {
assert_eq!(a.len(), b.len(), "{path}: {a:?} vs {b:?}");
for (x, y) in a.iter().zip(b) {
assert_eq!(x["token"], y["token"], "{path}: {x} vs {y}");
assert!((logprob(x) - logprob(y)).abs() < 1e-3, "{path}: {x} vs {y}");
}
}
async fn logprobs_are_served(state: &AppState, path: &str) {
let reply = chat_json(state, WITH).await;
let content = checked_entries(&reply, path);
let without = chat_json(state, "").await;
assert_eq!(
reply["choices"][0]["message"]["content"], without["choices"][0]["message"]["content"],
"{path}: asking for logprobs changed the reply"
);
assert_eq!(
reply["usage"]["completion_tokens"], without["usage"]["completion_tokens"],
"{path}"
);
let choice = without["choices"][0].as_object().expect("choice object");
assert!(!choice.contains_key("logprobs"), "{path}: {without}");
let (streamed, streamed_tokens) = streamed_entries(state, WITH).await;
assert_eq!(
streamed.len() as u64,
streamed_tokens,
"{path}: {streamed:?}"
);
same_entries(&streamed, content, path);
}
async fn batched_logprobs_are_served(state: &AppState) {
let (pair, counter) = batch_of_two(state);
let (reply, without) = tokio::join!(chat_json(&pair, WITH), chat_json(&pair, ""));
assert_eq!(
counter.peak_in_flight(),
2,
"the pair did not decode as one batch"
);
let content = checked_entries(&reply, "batched");
assert_eq!(
reply["choices"][0]["message"]["content"], without["choices"][0]["message"]["content"],
"batched: asking for logprobs changed the reply"
);
assert_eq!(
reply["usage"]["completion_tokens"], without["usage"]["completion_tokens"],
"batched"
);
let choice = without["choices"][0].as_object().expect("choice object");
assert!(!choice.contains_key("logprobs"), "batched: {without}");
let (pair, counter) = batch_of_two(state);
let ((streamed, streamed_tokens), again) =
tokio::join!(streamed_entries(&pair, WITH), chat_json(&pair, WITH));
assert_eq!(
counter.peak_in_flight(),
2,
"the streamed pair did not decode as one batch"
);
assert_eq!(
streamed.len() as u64,
streamed_tokens,
"batched: {streamed:?}"
);
let again = checked_entries(&again, "batched again");
same_entries(&streamed, again, "batched streamed");
same_entries(again, content, "batched again");
}
struct Turn {
max_tokens: usize,
tokens: tokio::sync::mpsc::Receiver<Result<u32, String>>,
records: Option<tokio::sync::mpsc::UnboundedReceiver<crate::gguf::logprobs::StepLogprobs>>,
}
fn turn(
prompt_ids: &[u32],
max_tokens: usize,
asks: bool,
) -> (crate::api::cuda_batch_scheduler::CudaBatchRequest, Turn) {
use crate::api::cuda_batch_scheduler::{CudaBatchRequest, RecordSink};
let (token_tx, tokens) = tokio::sync::mpsc::channel(64);
let (logprobs, records) = if asks {
let (records, rx) = tokio::sync::mpsc::unbounded_channel();
(Some(RecordSink { top_n: 3, records }), Some(rx))
} else {
(None, None)
};
let request = CudaBatchRequest {
prompt_ids: prompt_ids.to_vec(),
config: crate::gguf::QuantizedGenerateConfig::deterministic(max_tokens),
token_tx,
non_streaming: false,
enqueue_time: std::time::Instant::now(),
timing_tx: None,
logprobs,
};
let turn = Turn {
max_tokens,
tokens,
records,
};
(request, turn)
}
fn turn_is_served(mut turn: Turn, path: &str) {
let mut tokens = Vec::new();
while let Ok(token) = turn.tokens.try_recv() {
tokens.push(token.unwrap_or_else(|e| panic!("{path}: {e}")));
}
assert_eq!(tokens.len(), turn.max_tokens, "{path}: tokens {tokens:?}");
let Some(mut rx) = turn.records else {
return;
};
let mut records = Vec::new();
while let Ok(record) = rx.try_recv() {
records.push(record);
}
assert_eq!(records.len(), tokens.len(), "{path}: one record per token");
for (i, (record, &token)) in records.iter().zip(&tokens).enumerate() {
assert_eq!(record.step, i, "{path}");
assert_eq!(record.chosen, token, "{path}: step {i}");
assert_eq!(record.top.len(), 3, "{path}: step {i}");
assert!(
record.top.windows(2).all(|w| w[0].logprob >= w[1].logprob),
"{path}: step {i} not best first"
);
assert!(
record.chosen_logprob <= 0.0 && record.top.iter().all(|t| t.logprob <= 0.0),
"{path}: step {i}"
);
assert!(
(record.chosen_logprob - record.top[0].logprob).abs() < 1e-4,
"{path}: step {i} chose below the best at temperature 0"
);
}
}
fn iteration_scheduler_records(state: &AppState) {
use std::collections::VecDeque;
let model = state.cuda_model().expect("a CUDA state");
let (first, one) = turn(&[1, 450, 7483, 310, 3444, 338], 6, true);
let (second, two) = turn(&[1, 15043, 29892], 10, false);
let (third, three) = turn(&[1, 1724, 338], 8, true);
let (fourth, four) = turn(&[1, 9038, 2501, 263, 931], 8, true);
let (_open, mut rx) = tokio::sync::mpsc::channel(4);
let mut waiting = VecDeque::from([third, fourth]);
let mut iterations = 0;
crate::api::iteration_scheduler::process_iteration_batch(
model,
vec![first, second],
&mut rx,
&mut waiting,
3,
&mut iterations,
);
assert!(waiting.is_empty(), "iteration: {} never ran", waiting.len());
assert!(iterations > 1, "iteration: the batched path did not run");
turn_is_served(one, "iteration set up");
turn_is_served(two, "iteration set up, not asking");
turn_is_served(three, "iteration joined");
turn_is_served(four, "iteration recycled");
}
async fn traced_logprobs_are_refused(state: &AppState) {
let request = Request::builder()
.method("POST")
.uri(CHAT)
.header("content-type", "application/json")
.header("X-Trace-Level", "brick")
.body(Body::from(chat_body(r#","logprobs":true"#)))
.expect("build request");
let response = create_router(state.clone())
.oneshot(request)
.await
.expect("dispatch");
let status = response.status();
let body = body_string(response).await;
assert_eq!(status, StatusCode::NOT_IMPLEMENTED, "{body}");
assert!(body.contains("CUDA (traced)"), "{body}");
}
#[tokio::test]
async fn the_dense_cuda_backend_serves_chat_logprobs_direct_and_scheduled() {
let Some(direct) = cuda_state() else {
return;
};
logprobs_are_served(&direct, "direct").await;
traced_logprobs_are_refused(&direct).await;
logprobs_are_served(&scheduled(&direct), "scheduled").await;
batched_logprobs_are_served(&direct).await;
iteration_scheduler_records(&direct);
}