1use std::collections::BTreeMap;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use kernel::keep_warm::{CacheUse, KeepWarm, renewal_due};
19use kernel::{
20 AxError, B3Hash, Ceiling, EarlyCalls, Increments, Model, ModelRequest, ModelReturn, TimeMs,
21};
22
23#[derive(Debug, Clone)]
25pub struct Warmth {
26 setting: KeepWarm,
27 kept: BTreeMap<[B3Hash; 4], Kept>,
28}
29
30#[derive(Debug, Clone)]
32struct Kept {
33 request: ModelRequest<'static>,
34 cache: CacheUse,
35}
36
37impl Warmth {
38 pub fn new(setting: KeepWarm) -> Warmth {
39 Warmth {
40 setting,
41 kept: BTreeMap::new(),
42 }
43 }
44
45 pub fn sent(&mut self, request: &ModelRequest, at_ms: u64) {
47 match self.setting {
48 KeepWarm::Off => return,
49 KeepWarm::FiveMinute => {}
50 }
51 self.kept.insert(
52 request.segments,
53 Kept {
54 request: request.clone().into_owned(),
55 cache: CacheUse::sent(at_ms),
56 },
57 );
58 }
59
60 pub fn next_due(&self, lead: &Lead) -> Option<u64> {
64 self.kept
65 .values()
66 .filter_map(|kept| renewal_due(self.setting, kept.cache, lead.ms()))
67 .min()
68 }
69
70 pub fn renew_due(
77 &mut self,
78 lead: &Lead,
79 now_ms: u64,
80 mut send: impl FnMut(&ModelRequest) -> Result<ModelReturn, AxError>,
81 ) -> RenewalRound {
82 let (setting, lead_ms) = (self.setting, lead.ms());
83 self.kept
84 .retain(|_, kept| renewal_due(setting, kept.cache, lead_ms).is_some());
85 let mut round = RenewalRound::default();
86 for kept in self.kept.values_mut() {
87 if renewal_due(setting, kept.cache, lead_ms).is_some_and(|due| due <= now_ms) {
88 match send(&renewal_of(&kept.request)) {
89 Ok(answer) => {
90 round.answers.push(answer);
91 kept.cache = kept.cache.renewed();
92 }
93 Err(refused) => {
94 round.refused = Some(refused);
95 break;
96 }
97 }
98 }
99 }
100 round
101 }
102}
103
104#[derive(Debug, Default)]
108pub struct RenewalRound {
109 pub answers: Vec<ModelReturn>,
111 pub refused: Option<AxError>,
113}
114
115fn renewal_of(request: &ModelRequest<'static>) -> ModelRequest<'static> {
119 let mut renewal = request.clone();
120 renewal.chat.max_tokens = Ceiling::new(1);
121 renewal
122}
123
124#[derive(Debug, Clone, Default)]
133pub struct Lead(Arc<AtomicU64>);
134
135impl Lead {
136 pub fn ms(&self) -> u64 {
138 self.0.load(Ordering::Relaxed)
139 }
140
141 fn measured(&self, ms: u64) {
142 self.0.store(ms, Ordering::Relaxed);
143 }
144}
145
146pub struct Warmed<C> {
161 model: Box<dyn Model + Send>,
162 warmth: Warmth,
163 clock: C,
164 lead: Lead,
165}
166
167impl<C: FnMut() -> Result<TimeMs, AxError>> Warmed<C> {
168 pub fn new(model: Box<dyn Model + Send>, setting: KeepWarm, clock: C, lead: Lead) -> Warmed<C> {
171 Warmed {
172 model,
173 warmth: Warmth::new(setting),
174 clock,
175 lead,
176 }
177 }
178
179 pub fn next_due(&self) -> Option<u64> {
181 self.warmth.next_due(&self.lead)
182 }
183
184 pub fn renew_due(&mut self, now_ms: u64) -> RenewalRound {
189 let Warmed {
190 model,
191 warmth,
192 clock,
193 lead,
194 } = self;
195 let lead: &Lead = lead;
196 warmth.renew_due(lead, now_ms, |renewal| {
197 let left = clock()?;
198 let answer = model.call(renewal)?;
199 let back = clock()?;
200 lead.measured(back.value().saturating_sub(left.value()));
201 Ok(answer)
202 })
203 }
204
205 fn recorded(
211 &mut self,
212 request: &ModelRequest,
213 send: impl FnOnce(&mut dyn Model, Increments<'_>) -> Result<ModelReturn, AxError>,
214 ) -> Result<ModelReturn, AxError> {
215 let Warmed {
216 model,
217 warmth,
218 clock,
219 lead,
220 } = self;
221 let left = clock()?;
222 let mut first = None;
223 let answer = send(model.as_mut(), &mut |_| {
224 if first.is_none() {
225 first = Some(clock());
226 }
227 })?;
228 if let Some(first) = first {
229 lead.measured(first?.value().saturating_sub(left.value()));
230 }
231 warmth.sent(request, left.value());
232 Ok(answer)
233 }
234}
235
236impl<C: FnMut() -> Result<TimeMs, AxError>> Model for Warmed<C> {
237 fn provider_account(&self) -> Option<kernel::event::record::ProviderAccountBinding> {
238 self.model.provider_account()
239 }
240
241 fn account_roster(&self) -> Option<kernel::model::AccountRoster> {
242 self.model.account_roster()
243 }
244
245 fn select_account(&mut self, account: &kernel::ServerLabel) {
246 self.model.select_account(account);
247 }
248
249 fn prefix_held(&self) -> Option<kernel::event::record::PrefixHeld> {
250 self.model.prefix_held()
251 }
252
253 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
254 self.recorded(req, |model, _| model.call(req))
255 }
256
257 fn call_streaming(
258 &mut self,
259 req: &ModelRequest,
260 onto: Increments<'_>,
261 ) -> Result<ModelReturn, AxError> {
262 self.recorded(req, |model, first| {
263 model.call_streaming(req, &mut |increment| {
264 first(increment);
265 onto(increment);
266 })
267 })
268 }
269
270 fn call_speculating(
271 &mut self,
272 req: &ModelRequest,
273 onto: Increments<'_>,
274 early: EarlyCalls<'_>,
275 ) -> Result<ModelReturn, AxError> {
276 self.recorded(req, |model, first| {
277 model.call_speculating(
278 req,
279 &mut |increment| {
280 first(increment);
281 onto(increment);
282 },
283 early,
284 )
285 })
286 }
287}
288
289#[cfg(test)]
290#[allow(
291 clippy::unwrap_used,
292 clippy::expect_used,
293 clippy::indexing_slicing,
294 clippy::arithmetic_side_effects
295)]
296mod tests {
297 use super::*;
298 use kernel::consts_external::{PROMPT_CACHE_RENEWAL_MARGIN_SECS, PROMPT_CACHE_TTL_SECS};
299 use kernel::{BuildingPolicy, ChatRequest, ContentBlock, Increment};
300 use std::sync::Arc;
301 use std::sync::atomic::{AtomicU64, Ordering};
302
303 const TTL_MS: u64 = PROMPT_CACHE_TTL_SECS * 1000;
304 const MARGIN_MS: u64 = PROMPT_CACHE_RENEWAL_MARGIN_SECS * 1000;
305 const LEAD_MS: u64 = 2_000;
306 const USED_AT: u64 = 1_000_000;
307
308 #[derive(Default)]
310 struct Provider {
311 seen: Vec<ModelRequest<'static>>,
312 }
313
314 impl Model for Provider {
315 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
316 self.seen.push(req.clone().into_owned());
317 Ok(ModelReturn::bare(
318 kernel::model::message_payload(&[ContentBlock::Text {
319 text: "warm".to_owned(),
320 }])
321 .unwrap(),
322 Vec::new(),
323 ))
324 }
325 }
326
327 fn request() -> ModelRequest<'static> {
328 ModelRequest {
329 policy: BuildingPolicy::default(),
330 segments: [B3Hash::digest(b"prefix"); 4],
331 chat: ChatRequest::empty("script", Ceiling::new(512).unwrap()),
332 }
333 }
334
335 #[test]
336 fn a_later_refusal_keeps_the_answers_before_it() {
337 let mut warmth = Warmth::new(KeepWarm::FiveMinute);
338 let mut first = request();
339 first.segments = [B3Hash::digest(b"one"); 4];
340 let mut second = request();
341 second.segments = [B3Hash::digest(b"two"); 4];
342 warmth.sent(&first, USED_AT);
343 warmth.sent(&second, USED_AT);
344 let due = warmth.next_due(&lead()).unwrap();
345 let mut calls = 0;
346 let round = warmth.renew_due(&lead(), due, |_| {
347 calls += 1;
348 if calls == 1 {
349 Ok(ModelReturn::bare(
350 kernel::model::message_payload(&[ContentBlock::Text {
351 text: "warm".to_owned(),
352 }])
353 .unwrap(),
354 Vec::new(),
355 ))
356 } else {
357 Err(AxError::failure(
358 kernel::AxCode::Provider,
359 "renew a cached prefix",
360 "rate limited",
361 )
362 .with_recovery("renewal is skipped until the next real request"))
363 }
364 });
365 assert_eq!(round.answers.len(), 1, "the answered renewal survives");
366 assert!(round.refused.is_some(), "the refusal ends the round");
367 assert_eq!(calls, 2, "nothing more is sent after the refusal");
368 assert!(
369 warmth.next_due(&lead()).is_some(),
370 "the refused prefix stays"
371 );
372 }
373
374 fn lead() -> Lead {
376 let lead = Lead::default();
377 lead.measured(LEAD_MS);
378 lead
379 }
380
381 fn wake_through_two_lifetimes(warmth: &mut Warmth, provider: &mut Provider) {
384 for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
385 let round = warmth.renew_due(&lead(), now, |renewal| provider.call(renewal));
386 assert!(round.refused.is_none(), "{:?}", round.refused);
387 }
388 }
389
390 #[test]
391 fn the_default_setting_sends_no_request_of_its_own() {
392 let mut warmth = Warmth::new(KeepWarm::default());
393 let mut provider = Provider::default();
394 warmth.sent(&request(), USED_AT);
395 assert_eq!(warmth.next_due(&lead()), None);
396 wake_through_two_lifetimes(&mut warmth, &mut provider);
397 assert!(
398 provider.seen.is_empty(),
399 "{} renewals sent",
400 provider.seen.len()
401 );
402 }
403
404 #[test]
405 fn five_minute_resends_the_last_request_once_with_one_output_token() {
406 let mut warmth = Warmth::new(KeepWarm::FiveMinute);
407 let mut provider = Provider::default();
408 warmth.sent(&request(), USED_AT);
409 assert_eq!(
410 warmth.next_due(&lead()),
411 Some(USED_AT + TTL_MS - MARGIN_MS - LEAD_MS)
412 );
413 wake_through_two_lifetimes(&mut warmth, &mut provider);
414 let mut renewal = request();
415 renewal.chat.max_tokens = Ceiling::new(1);
416 assert_eq!(provider.seen, vec![renewal]);
417 assert_eq!(warmth.next_due(&lead()), None);
418 }
419
420 type Seen = std::sync::Arc<std::sync::Mutex<Vec<ModelRequest<'static>>>>;
423
424 struct Behind(Seen);
425
426 impl Model for Behind {
427 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
428 let mut provider = Provider::default();
429 let answer = provider.call(req);
430 self.0.lock().unwrap().push(req.clone().into_owned());
431 answer
432 }
433 }
434
435 fn door(setting: KeepWarm) -> (Warmed<impl FnMut() -> Result<TimeMs, AxError>>, Seen) {
436 let seen = Seen::default();
437 let clock = || Ok(TimeMs::new(USED_AT));
438 let warmed = Warmed::new(
439 Box::new(Behind(seen.clone())),
440 setting,
441 clock,
442 Lead::default(),
443 );
444 (warmed, seen)
445 }
446
447 fn wake_door_through_two_lifetimes(
448 warmed: &mut Warmed<impl FnMut() -> Result<TimeMs, AxError>>,
449 ) {
450 for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
451 let round = warmed.renew_due(now);
452 assert!(round.refused.is_none(), "{:?}", round.refused);
453 }
454 }
455
456 #[test]
457 fn the_default_door_forwards_a_run_and_sends_nothing_of_its_own() {
458 let (mut warmed, seen) = door(KeepWarm::default());
459 warmed.call(&request()).unwrap();
460 wake_door_through_two_lifetimes(&mut warmed);
461 assert_eq!(*seen.lock().unwrap(), vec![request()]);
462 }
463
464 #[test]
465 fn a_five_minute_door_renews_what_a_run_sent_through_it_once() {
466 let (mut warmed, seen) = door(KeepWarm::FiveMinute);
467 warmed.call_streaming(&request(), &mut |_| {}).unwrap();
468 assert_eq!(warmed.next_due(), Some(USED_AT + TTL_MS - MARGIN_MS));
469 wake_door_through_two_lifetimes(&mut warmed);
470 let mut renewal = request();
471 renewal.chat.max_tokens = Ceiling::new(1);
472 assert_eq!(*seen.lock().unwrap(), vec![request(), renewal]);
473 }
474
475 const CALL_MS: u64 = 200_000;
477 const FIRST_WORD_MS: u64 = 3_000;
479 const RENEWAL_MS: u64 = 2_000;
481
482 struct Slow {
487 now: Arc<AtomicU64>,
488 seen: Seen,
489 }
490
491 impl Slow {
492 fn answer(
493 &mut self,
494 req: &ModelRequest,
495 onto: Increments<'_>,
496 ) -> Result<ModelReturn, AxError> {
497 self.seen.lock().unwrap().push(req.clone().into_owned());
498 if req.chat.max_tokens == Ceiling::new(1) {
499 self.now.fetch_add(RENEWAL_MS, Ordering::SeqCst);
500 } else {
501 self.now.fetch_add(FIRST_WORD_MS, Ordering::SeqCst);
502 onto(&Increment::Said("warm".to_owned()));
503 self.now
504 .fetch_add(CALL_MS - FIRST_WORD_MS, Ordering::SeqCst);
505 }
506 Provider::default().call(req)
507 }
508 }
509
510 impl Model for Slow {
511 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
512 self.answer(req, &mut |_| {})
513 }
514
515 fn call_streaming(
516 &mut self,
517 req: &ModelRequest,
518 onto: Increments<'_>,
519 ) -> Result<ModelReturn, AxError> {
520 self.answer(req, onto)
521 }
522 }
523
524 fn slow_door(
527 lead: Lead,
528 ) -> (
529 Warmed<impl FnMut() -> Result<TimeMs, AxError>>,
530 Arc<AtomicU64>,
531 Seen,
532 ) {
533 let (now, seen) = (Arc::new(AtomicU64::new(USED_AT)), Seen::default());
534 let provider = Slow {
535 now: now.clone(),
536 seen: seen.clone(),
537 };
538 let read = now.clone();
539 let clock = move || Ok(TimeMs::new(read.load(Ordering::SeqCst)));
540 let warmed = Warmed::new(Box::new(provider), KeepWarm::FiveMinute, clock, lead);
541 (warmed, now, seen)
542 }
543
544 #[test]
545 fn a_long_call_leaves_its_renewal_due_a_lifetime_after_it_was_sent() {
546 let (mut warmed, now, seen) = slow_door(Lead::default());
547 warmed.call(&request()).unwrap();
548 let landed = now.load(Ordering::SeqCst);
549 assert_eq!(warmed.next_due(), Some(USED_AT + TTL_MS - MARGIN_MS));
550 assert!(warmed.renew_due(landed).refused.is_none());
551 assert_eq!(
552 *seen.lock().unwrap(),
553 vec![request()],
554 "renewed at the landing"
555 );
556
557 let due = warmed.next_due().unwrap();
558 now.store(due, Ordering::SeqCst);
559 assert!(warmed.renew_due(due).refused.is_none());
560 let sent_again = now.load(Ordering::SeqCst);
561 warmed.call(&request()).unwrap();
562 assert_eq!(
563 warmed.next_due(),
564 Some(sent_again + TTL_MS - RENEWAL_MS - MARGIN_MS)
565 );
566 }
567
568 #[test]
569 fn a_streamed_call_reads_the_lead_at_its_first_word() {
570 let (mut warmed, _, _) = slow_door(Lead::default());
571 let mut words = 0;
572 warmed
573 .call_streaming(&request(), &mut |_| words += 1)
574 .unwrap();
575 assert_eq!(
576 (words, warmed.next_due()),
577 (1, Some(USED_AT + TTL_MS - FIRST_WORD_MS - MARGIN_MS))
578 );
579 }
580
581 #[test]
582 fn doors_to_one_endpoint_share_its_lead() {
583 let lead = Lead::default();
584 let (mut renewing, now, _) = slow_door(lead.clone());
585 let (mut waiting, _, _) = slow_door(lead);
586 waiting.call(&request()).unwrap();
587 renewing.call(&request()).unwrap();
588 let due = renewing.next_due().unwrap();
589 now.store(due, Ordering::SeqCst);
590 assert!(renewing.renew_due(due).refused.is_none());
591 assert_eq!(
592 waiting.next_due(),
593 Some(USED_AT + TTL_MS - MARGIN_MS - RENEWAL_MS)
594 );
595 }
596}