1#![deny(warnings)]
4#![deny(clippy::all)]
5
6#[cfg(feature = "cuda")]
15pub mod gpu;
16
17use std::io::BufRead;
18use std::sync::Barrier;
19use std::time::{Duration, Instant};
20
21use anyhow::{Context, Result};
22use serde::{Deserialize, Serialize};
23
24pub fn server_url() -> String {
27 std::env::var("ATLAS_BENCH_URL").unwrap_or_else(|_| "http://localhost:8888".into())
28}
29
30pub fn require_server() -> String {
31 let url = server_url();
32 match ureq::get(&format!("{url}/health")).call() {
33 Ok(resp) if resp.status() == 200 => url,
34 Ok(resp) => panic!("Server at {url} returned status {}", resp.status()),
35 Err(e) => panic!("Server not reachable at {url}: {e}. Start Atlas Spark first."),
36 }
37}
38
39#[derive(Serialize)]
42struct ChatRequest<'a> {
43 model: &'a str,
44 messages: Vec<Message<'a>>,
45 max_tokens: usize,
46 temperature: f32,
47 stream: bool,
48}
49
50#[derive(Serialize)]
51struct Message<'a> {
52 role: &'a str,
53 content: &'a str,
54}
55
56#[derive(Deserialize)]
57struct ChatResponse {
58 choices: Vec<ResponseChoice>,
59 usage: ResponseUsage,
60}
61
62#[derive(Deserialize)]
63struct ResponseChoice {
64 message: ResponseMessage,
65 finish_reason: String,
66}
67
68#[derive(Deserialize)]
69struct ResponseMessage {
70 content: String,
71}
72
73#[derive(Deserialize)]
74struct ResponseUsage {
75 prompt_tokens: usize,
76 completion_tokens: usize,
77}
78
79#[derive(Deserialize)]
80struct ChunkPayload {
81 choices: Vec<ChunkChoice>,
82}
83
84#[derive(Deserialize)]
85struct ChunkChoice {
86 delta: ChunkDelta,
87 finish_reason: Option<String>,
88}
89
90#[derive(Deserialize)]
91struct ChunkDelta {
92 content: Option<String>,
93}
94
95pub struct BlockingResult {
98 pub text: String,
99 pub prompt_tokens: usize,
100 pub completion_tokens: usize,
101 pub finish_reason: String,
102 pub elapsed: Duration,
103}
104
105pub struct StreamResult {
106 pub text: String,
107 pub token_count: usize,
108 pub ttft: Duration,
109 pub decode_duration: Duration,
110 pub total_duration: Duration,
111 pub decode_tok_s: f64,
112 pub finish_reason: String,
113}
114
115pub fn send_blocking(url: &str, prompt: &str, max_tokens: usize) -> Result<BlockingResult> {
118 let body = ChatRequest {
119 model: "qwen3",
120 messages: vec![Message {
121 role: "user",
122 content: prompt,
123 }],
124 max_tokens,
125 temperature: 0.0,
126 stream: false,
127 };
128
129 let t_start = Instant::now();
130 let resp = ureq::post(&format!("{url}/v1/chat/completions"))
131 .send_json(&body)
132 .context("POST request failed")?;
133
134 let (_, mut body) = resp.into_parts();
135 let parsed: ChatResponse = body.read_json().context("Failed to parse response JSON")?;
136 let elapsed = t_start.elapsed();
137
138 let choice = parsed
139 .choices
140 .into_iter()
141 .next()
142 .context("No choices in response")?;
143
144 Ok(BlockingResult {
145 text: choice.message.content,
146 prompt_tokens: parsed.usage.prompt_tokens,
147 completion_tokens: parsed.usage.completion_tokens,
148 finish_reason: choice.finish_reason,
149 elapsed,
150 })
151}
152
153pub fn send_streaming(url: &str, prompt: &str, max_tokens: usize) -> Result<StreamResult> {
156 let body = ChatRequest {
157 model: "qwen3",
158 messages: vec![Message {
159 role: "user",
160 content: prompt,
161 }],
162 max_tokens,
163 temperature: 0.0,
164 stream: true,
165 };
166
167 let t_start = Instant::now();
168 let resp = ureq::post(&format!("{url}/v1/chat/completions"))
169 .send_json(&body)
170 .context("POST streaming request failed")?;
171
172 let (_, body) = resp.into_parts();
173 let reader = std::io::BufReader::new(body.into_reader());
174 let mut token_count: usize = 0;
175 let mut t_first: Option<Instant> = None;
176 let mut t_last: Option<Instant> = None;
177 let mut finish_reason = String::new();
178 let mut text_parts = Vec::new();
179
180 for line_result in reader.lines() {
181 let line: String = line_result.context("Failed to read SSE line")?;
182 let Some(payload) = line.strip_prefix("data: ") else {
183 continue;
184 };
185 if payload == "[DONE]" {
186 break;
187 }
188 let chunk: ChunkPayload = match serde_json::from_str(payload) {
189 Ok(c) => c,
190 Err(_) => continue,
191 };
192 let Some(choice) = chunk.choices.first() else {
193 continue;
194 };
195 if let Some(ref fr) = choice.finish_reason {
196 finish_reason = fr.clone();
197 }
198 if let Some(ref content) = choice.delta.content {
199 let now = Instant::now();
200 if t_first.is_none() {
201 t_first = Some(now);
202 }
203 t_last = Some(now);
204 token_count += 1;
205 text_parts.push(content.clone());
206 }
207 }
208
209 let total_duration = t_start.elapsed();
210 let ttft = t_first.map(|tf| tf - t_start).unwrap_or(total_duration);
211 let decode_duration = match (t_first, t_last) {
212 (Some(tf), Some(tl)) if tl > tf => tl - tf,
213 _ => Duration::ZERO,
214 };
215 let decode_tok_s = if !decode_duration.is_zero() && token_count >= 2 {
216 (token_count - 1) as f64 / decode_duration.as_secs_f64()
217 } else {
218 0.0
219 };
220
221 Ok(StreamResult {
222 text: text_parts.concat(),
223 token_count,
224 ttft,
225 decode_duration,
226 total_duration,
227 decode_tok_s,
228 finish_reason,
229 })
230}
231
232pub fn send_concurrent_streaming(
235 url: &str,
236 prompt: &str,
237 max_tokens: usize,
238 concurrency: usize,
239) -> Vec<Result<StreamResult>> {
240 let barrier = Barrier::new(concurrency);
241 std::thread::scope(|s| {
242 let handles: Vec<_> = (0..concurrency)
243 .map(|_| {
244 s.spawn(|| {
245 barrier.wait();
246 send_streaming(url, prompt, max_tokens)
247 })
248 })
249 .collect();
250 handles.into_iter().map(|h| h.join().unwrap()).collect()
251 })
252}
253
254pub fn short_prompt() -> &'static str {
257 "What is the capital of France?"
258}
259
260pub fn medium_prompt() -> String {
261 "Explain quantum computing. ".repeat(4) + "Be concise."
262}
263
264pub fn long_prompt() -> String {
265 "Explain quantum computing. ".repeat(16) + "Be concise."
266}
267
268pub fn very_long_prompt() -> String {
269 "Explain quantum computing. ".repeat(32) + "Be concise."
270}