1use std::{
35 collections::{BTreeSet, HashMap, HashSet},
36 sync::{
37 atomic::{AtomicBool, Ordering},
38 mpsc, Arc, Condvar, Mutex, MutexGuard,
39 },
40 thread,
41 time::{Duration, Instant},
42};
43
44use bytes::Bytes;
45use ts_collective::{Group, Mesh, Reduce};
46
47use crate::{
48 batch::{BatchRows, Staged},
49 control::{BootstrapInfo, PdError, PdEvent, TransferPeer},
50 http,
51 layout::{
52 contract::Contract,
53 copies::{Source, Transfer},
54 manifest::{build_manifest, build_selection, validate_manifest, Manifest, Selection},
55 registration::{
56 plan_registration, PlannedRegistration, Preallocation, PrefillLayout, RegistrationInfo,
57 },
58 },
59 status::{RoomTable, TransferPoll},
60 wire::{self, StatusMessage},
61 Copy, PeerConfig, StartError, Transport,
62};
63
64const BOOTSTRAP_STALL: Duration = Duration::from_secs(10);
66
67const SOCKET_POLL: Duration = Duration::from_millis(100);
69
70#[derive(Debug, Clone, PartialEq)]
72struct Registration {
73 endpoint: String,
75 port: u16,
77 session_id: String,
79 decode_tp_size: i64,
81 decode_tp_rank: i64,
83 arena_base: u64,
85 layout: Arc<Contract>,
87 planned: Arc<PlannedRegistration>,
89}
90
91impl Registration {
92 fn is_dummy(&self) -> bool {
93 self.planned.is_dummy
94 }
95}
96
97#[derive(Debug, Clone)]
100pub struct Layerwise {
101 pub step_address: usize,
104 pub fields_by_step: Vec<Vec<String>>,
107 pub interval: usize,
109}
110
111impl Layerwise {
112 fn interval_fields(&self) -> Vec<Option<BTreeSet<String>>> {
114 let interval = self.interval.max(1);
115 self.fields_by_step
116 .chunks(interval)
117 .map(|steps| Some(steps.iter().flatten().cloned().collect()))
118 .collect()
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct PrefillConfig {
125 pub peer: PeerConfig,
127 pub layout: PrefillLayout,
129 pub status_rank: i64,
131 pub bootstrap_url: String,
133 pub bootstrap_payload: serde_json::Value,
135 pub bootstrap_timeout: Duration,
138 pub failed_session_ttl: Duration,
141 pub transfer_workers: usize,
143 pub layerwise: Option<Layerwise>,
145}
146
147#[derive(Debug, Clone)]
149struct Destination {
150 session_id: String,
151 manifest: Option<Arc<Manifest>>,
154}
155
156impl Destination {
157 fn is_dummy(&self) -> bool {
158 self.manifest.is_none()
159 }
160}
161
162enum Chunk {
164 Full {
166 room: u64,
167 source: Option<Arc<Manifest>>,
170 bootstrap_token: i64,
171 spec_candidate_ids: Option<Vec<i32>>,
172 },
173 Layerwise {
177 room: u64,
178 selection: Arc<Selection>,
180 target_steps: Vec<u64>,
182 is_last: bool,
183 },
184}
185
186impl Chunk {
187 fn room(&self) -> u64 {
188 match self {
189 Chunk::Full { room, .. } | Chunk::Layerwise { room, .. } => *room,
190 }
191 }
192}
193
194const STEP_COUNT_MAX: u64 = 1 << 62;
196
197fn step_ready(current: u64, target: u64) -> bool {
200 target != current && (target + STEP_COUNT_MAX - current) % STEP_COUNT_MAX > STEP_COUNT_MAX / 2
201}
202
203#[allow(unsafe_code)]
206fn ready_step(address: usize) -> u64 {
207 unsafe { std::ptr::read_volatile(address as *const i64) as u64 }
210}
211
212#[derive(Default)]
213struct State {
214 rooms: RoomTable,
215 tracked: HashSet<u64>,
218 rooms_by_rid: HashMap<String, u64>,
220 unclaimed: HashMap<u64, Instant>,
225 registrations: HashMap<String, Registration>,
226 rejected: HashMap<String, (String, u16)>,
228 destinations: HashMap<u64, Vec<Destination>>,
229 failed_sessions: HashMap<String, Instant>,
230 cached_tokens: HashMap<u64, i64>,
231 bootstrap_logprobs: HashMap<u64, f64>,
232 layerwise_rooms: HashSet<u64>,
234 prefill_metadata: HashMap<u64, (i64, Option<Vec<i32>>)>,
236}
237
238impl State {
239 fn route_of(&self, session_id: &str) -> Option<(String, u16)> {
240 self.registrations
241 .get(session_id)
242 .map(|registration| (registration.endpoint.clone(), registration.port))
243 .or_else(|| self.rejected.get(session_id).cloned())
244 }
245
246 fn ended(&self, room: u64) -> bool {
247 matches!(self.rooms.get(room), None | Some(TransferPoll::Failed))
248 }
249
250 fn touch(&mut self, room: u64) {
253 if !self.tracked.contains(&room) {
254 self.unclaimed.entry(room).or_insert_with(Instant::now);
255 }
256 }
257
258 #[allow(clippy::type_complexity)]
262 fn take_unclaimed(&mut self, ttl: Duration) -> (Vec<u64>, Vec<((String, u16), u64)>) {
263 let mut expired: Vec<u64> = self
264 .unclaimed
265 .iter()
266 .filter(|(_, since)| since.elapsed() >= ttl)
267 .map(|(&room, _)| room)
268 .collect();
269 expired.sort_unstable();
270 let mut notices = Vec::new();
271 for &room in &expired {
272 if self.rooms.get(room) != Some(TransferPoll::Failed) {
273 let destinations = self.destinations.get(&room).map_or(&[][..], Vec::as_slice);
274 notices.extend(
275 destinations
276 .iter()
277 .filter(|destination| !destination.is_dummy())
278 .filter_map(|destination| self.route_of(&destination.session_id))
279 .map(|route| (route, room)),
280 );
281 }
282 self.clear(room);
283 }
284 (expired, notices)
285 }
286
287 fn clear(&mut self, room: u64) {
289 self.rooms.remove(room);
290 self.tracked.remove(&room);
291 self.unclaimed.remove(&room);
292 self.destinations.remove(&room);
293 self.cached_tokens.remove(&room);
294 self.bootstrap_logprobs.remove(&room);
295 self.layerwise_rooms.remove(&room);
296 self.prefill_metadata.remove(&room);
297 }
298
299 fn validate_fanout(&self, destinations: &[Destination]) -> Result<(), String> {
301 let registrations = destinations
302 .iter()
303 .map(|destination| {
304 self.registrations
305 .get(&destination.session_id)
306 .ok_or_else(|| {
307 "CachePD request references an unregistered Decode session".to_owned()
308 })
309 })
310 .collect::<Result<Vec<_>, _>>()?;
311 let sizes: BTreeSet<_> = registrations.iter().map(|r| r.decode_tp_size).collect();
312 let expected: BTreeSet<_> = registrations
313 .iter()
314 .map(|r| r.planned.expected_decode_ranks.clone())
315 .collect();
316 if sizes.len() != 1 || expected.len() != 1 {
317 return Err("Cache-transfer room has inconsistent TP metadata".to_owned());
318 }
319 let expected: BTreeSet<i64> = expected.into_iter().next().expect("one set");
320 let actual: BTreeSet<i64> = registrations.iter().map(|r| r.decode_tp_rank).collect();
321 if destinations.len() != expected.len() || actual != expected {
322 return Err(
323 "Cache-transfer room destination ranks disagree with the typed route plan"
324 .to_owned(),
325 );
326 }
327 Ok(())
328 }
329}
330
331struct Shared {
332 transport: Arc<Transport>,
333 layout: PrefillLayout,
334 state: Mutex<State>,
335 status_rank: i64,
336 failed_session_ttl: Duration,
337 metrics: Option<Arc<ts_metrics::KvTransferMetrics>>,
338 layerwise: Option<Layerwise>,
339 interval_fields: Vec<Option<BTreeSet<String>>>,
341 metadata_ready: Condvar,
343}
344
345impl Shared {
346 fn state(&self) -> MutexGuard<'_, State> {
347 self.state.lock().expect("the PD state lock is poisoned")
348 }
349
350 fn notify(&self, (endpoint, port): (String, u16), message: &StatusMessage) {
351 self.transport
352 .sockets
353 .send(&endpoint, port, message.to_frames());
354 }
355
356 fn notify_failed(&self, route: (String, u16), room: u64) {
357 self.notify(
358 route,
359 &StatusMessage::bare(room, TransferPoll::Failed, self.status_rank),
360 );
361 }
362
363 fn abort_room(&self, room: u64, reason: &str) {
366 let routes: Vec<_> = {
367 let mut state = self.state();
368 let counted = state.rooms.get(room) == Some(TransferPoll::Failed)
370 || state.rooms.has_failure(room);
371 if let (false, Some(metrics)) = (counted, &self.metrics) {
372 metrics.record_failure();
373 }
374 state.rooms.record_failure(room, reason);
375 state.rooms.update(room, TransferPoll::Failed);
376 state.touch(room);
377 let destinations = state.destinations.remove(&room).unwrap_or_default();
378 destinations
379 .iter()
380 .filter(|destination| !destination.is_dummy())
381 .filter_map(|destination| {
382 let route = state.route_of(&destination.session_id);
383 if route.is_none() {
384 tracing::error!(
385 room,
386 session = destination.session_id,
387 "cannot notify Decode of a failed room: unregistered session"
388 );
389 }
390 route
391 })
392 .collect()
393 };
394 for route in routes {
395 self.notify_failed(route, room);
396 }
397 }
398
399 fn handle(&self, frames: &[Bytes]) {
400 if wire::is_registration(frames) {
401 self.handle_registration(frames);
402 } else {
403 self.handle_preallocation(frames);
404 }
405 }
406
407 fn plan_registration(&self, frames: &[Bytes]) -> Result<Registration, String> {
409 let info = RegistrationInfo::from_frames(frames).map_err(|error| error.0)?;
410 let planned = plan_registration(&self.layout, &info).map_err(|error| error.0)?;
411 Ok(Registration {
412 endpoint: info.endpoint,
413 port: info.port,
414 session_id: info.session_id,
415 decode_tp_size: info.tp_size,
416 decode_tp_rank: info.tp_rank,
417 arena_base: info.arena_base,
418 layout: Arc::new(info.layout),
419 planned: Arc::new(planned),
420 })
421 }
422
423 fn handle_registration(&self, frames: &[Bytes]) {
424 let registered = self.plan_registration(frames).and_then(|registration| {
425 let mut state = self.state();
426 let session_id = registration.session_id.clone();
427 if state
428 .registrations
429 .get(&session_id)
430 .is_some_and(|previous| *previous != registration)
431 {
432 return Err("conflicting registration for an active CachePD session".to_owned());
433 }
434 state.rejected.remove(&session_id);
435 state.failed_sessions.remove(&session_id);
436 state.registrations.insert(session_id, registration);
437 Ok(())
438 });
439 match registered {
440 Ok(()) => {}
441 Err(reason) => {
442 tracing::error!(reason, "rejecting a CachePD Decode registration");
443 if let Ok((endpoint, port, session_id)) = wire::registration_route(frames) {
444 self.reject_session(endpoint, port, &session_id, &reason);
445 }
446 }
447 }
448 }
449
450 fn reject_session(&self, endpoint: String, port: u16, session_id: &str, reason: &str) {
452 let affected: Vec<u64> = self
453 .state()
454 .destinations
455 .iter()
456 .filter(|(_, destinations)| {
457 destinations
458 .iter()
459 .any(|destination| destination.session_id == session_id)
460 })
461 .map(|(&room, _)| room)
462 .collect();
463 for room in affected {
464 self.abort_room(
465 room,
466 &format!("Decode session {session_id} registration rejected: {reason}"),
467 );
468 }
469 let mut state = self.state();
470 state.registrations.remove(session_id);
471 state
472 .rejected
473 .insert(session_id.to_owned(), (endpoint, port));
474 }
475
476 fn handle_preallocation(&self, frames: &[Bytes]) {
477 let (room, session_id) = match wire::preallocation_route(frames) {
478 Ok(route) => route,
479 Err(error) => {
480 tracing::error!(%error, "rejecting a malformed CachePD pre-allocation");
481 return;
482 }
483 };
484 if let Err(reason) = self.accept_preallocation(room, &session_id, frames) {
485 tracing::error!(room, reason, "rejecting CachePD pre-allocation metadata");
486 self.abort_room(room, &format!("invalid pre-allocation metadata: {reason}"));
487 match self.state().route_of(&session_id) {
488 Some(route) => self.notify_failed(route, room),
489 None => tracing::error!(room, "cannot notify a malformed Decode peer"),
490 }
491 }
492 }
493
494 fn accept_preallocation(
495 &self,
496 room: u64,
497 session_id: &str,
498 frames: &[Bytes],
499 ) -> Result<(), String> {
500 let registration = self
501 .state()
502 .registrations
503 .get(session_id)
504 .cloned()
505 .ok_or("CachePD request references an unregistered Decode session")?;
506 let route = (registration.endpoint.clone(), registration.port);
507 if self.state().rooms.get(room) == Some(TransferPoll::Failed) {
508 self.notify_failed(route, room);
509 return Ok(());
510 }
511 let preallocation = Preallocation::from_frames(frames).map_err(|error| error.0)?;
512 if preallocation.manifest.is_none() != registration.is_dummy() {
513 return Err("CachePD request kind disagrees with its registration".to_owned());
514 }
515 if let Some(manifest) = &preallocation.manifest {
516 validate_manifest(manifest, ®istration.layout, "destination")
517 .map_err(|error| error.0)?;
518 }
519 let mut state = self.state();
520 let mut destinations = state.destinations.get(&room).cloned().unwrap_or_default();
521 if destinations
522 .iter()
523 .any(|destination| destination.session_id == session_id)
524 {
525 return Err("duplicate CachePD pre-allocation for Decode session".to_owned());
526 }
527 destinations.push(Destination {
528 session_id: session_id.to_owned(),
529 manifest: preallocation.manifest.map(Arc::new),
530 });
531 let expected = registration.planned.expected_decode_ranks.len();
532 if destinations.len() > expected {
533 return Err("pre-allocation exceeds destination fanout".to_owned());
534 }
535 let complete = destinations.len() == expected;
536 if complete {
537 state.validate_fanout(&destinations)?;
538 }
539 if state.rooms.get(room) == Some(TransferPoll::Failed) {
541 drop(state);
542 self.notify_failed(route, room);
543 return Ok(());
544 }
545 state.destinations.insert(room, destinations);
546 state.touch(room);
547 if complete {
548 state.rooms.update(room, TransferPoll::Bootstrapped);
549 }
550 Ok(())
551 }
552
553 fn drop_unclaimed(&self, ttl: Duration) {
556 let (expired, notices) = self.state().take_unclaimed(ttl);
557 if !expired.is_empty() {
558 tracing::warn!(
559 rooms = ?expired,
560 ?ttl,
561 "dropping rooms no request on this rank claimed"
562 );
563 }
564 for (route, room) in notices {
565 self.notify_failed(route, room);
566 }
567 }
568
569 fn session_failed(&self, session_id: &str) -> bool {
570 if self.failed_session_ttl.is_zero() {
571 return false;
572 }
573 let mut state = self.state();
574 match state.failed_sessions.get(session_id) {
575 None => false,
576 Some(at) if at.elapsed() < self.failed_session_ttl => true,
577 Some(_) => {
578 state.failed_sessions.remove(session_id);
579 false
580 }
581 }
582 }
583
584 fn mark_session_failed(&self, session_id: &str) {
585 if self.failed_session_ttl.is_zero() {
586 return;
587 }
588 tracing::warn!(
589 session = session_id,
590 ttl = ?self.failed_session_ttl,
591 "Decode session marked failed"
592 );
593 self.state()
594 .failed_sessions
595 .insert(session_id.to_owned(), Instant::now());
596 }
597
598 fn transfer(&self, chunk: &Chunk) -> Result<(), String> {
600 match chunk {
601 Chunk::Full {
602 room,
603 source,
604 bootstrap_token,
605 spec_candidate_ids,
606 } => self.transfer_full(
607 *room,
608 source.as_deref(),
609 *bootstrap_token,
610 spec_candidate_ids,
611 ),
612 Chunk::Layerwise {
613 room,
614 selection,
615 target_steps,
616 is_last,
617 } => self.transfer_layerwise(*room, selection, target_steps, *is_last),
618 }
619 }
620
621 fn plan_copies(
624 &self,
625 registration: &Registration,
626 source: Source<'_>,
627 destination: &Destination,
628 steps: &[Option<BTreeSet<String>>],
629 ) -> Result<Vec<Vec<Copy>>, String> {
630 let manifest = destination
631 .manifest
632 .as_deref()
633 .ok_or("a dummy destination has no manifest to send to")?;
634 Transfer {
635 layout: &self.layout.local,
636 arena_base: self.layout.arena_base,
637 destination_base: registration.arena_base,
638 destination_tp_rank: registration.decode_tp_rank,
639 destination_layout: ®istration.layout,
640 source,
641 destination: manifest,
642 fragments: ®istration.planned.fragments,
643 owner_filters: ®istration.planned.owner_filters,
644 }
645 .copies(steps)
646 .map_err(|error| error.0)
647 }
648
649 fn live_registrations(
651 &self,
652 destinations: &[Destination],
653 ) -> Result<Vec<Registration>, String> {
654 destinations
655 .iter()
656 .map(|destination| {
657 if self.session_failed(&destination.session_id) {
658 return Err(format!(
659 "Decode session {} is not alive",
660 destination.session_id
661 ));
662 }
663 self.state()
664 .registrations
665 .get(&destination.session_id)
666 .cloned()
667 .ok_or_else(|| {
668 "CachePD request references an unregistered Decode session".to_owned()
669 })
670 })
671 .collect()
672 }
673
674 fn write_copies(&self, registration: &Registration, copies: &[Copy]) -> Result<(), String> {
675 let started = Instant::now();
676 if let Err(error) = self.transport.write(®istration.session_id, copies) {
677 self.mark_session_failed(®istration.session_id);
678 return Err(format!(
679 "Mooncake transfer failed for {}:{}: {error}",
680 registration.endpoint, registration.port
681 ));
682 }
683 if let Some(metrics) = &self.metrics {
684 metrics.observe_latency(started.elapsed().as_secs_f64());
685 }
686 Ok(())
687 }
688
689 fn report_success(
692 &self,
693 room: u64,
694 registrations: &[Registration],
695 bootstrap_token: i64,
696 spec_candidate_ids: &Option<Vec<i32>>,
697 ) {
698 let (cached_tokens, bootstrap_logprob) = {
699 let state = self.state();
700 if state.ended(room) {
701 return;
702 }
703 (
704 state.cached_tokens.get(&room).copied().unwrap_or(0),
705 state.bootstrap_logprobs.get(&room).copied(),
706 )
707 };
708 let message = StatusMessage {
709 room,
710 status: TransferPoll::Success,
711 prefill_rank: self.status_rank,
712 bootstrap_token,
713 spec_candidate_ids: spec_candidate_ids.clone(),
714 cached_tokens,
715 bootstrap_logprob,
716 };
717 for registration in registrations {
718 self.notify((registration.endpoint.clone(), registration.port), &message);
719 }
720 let mut state = self.state();
721 state.rooms.update(room, TransferPoll::Success);
722 state.destinations.remove(&room);
723 }
724
725 fn wait_for_step(
728 &self,
729 address: usize,
730 target: u64,
731 room: u64,
732 sessions: &[String],
733 ) -> Result<(), String> {
734 let mut next_session_check = Instant::now();
735 loop {
736 if self.state().ended(room) {
737 return Err(format!(
738 "Cache-transfer layerwise wait aborted for room {room}"
739 ));
740 }
741 let now = Instant::now();
742 if !sessions.is_empty() && now >= next_session_check {
743 if sessions.iter().any(|session| self.session_failed(session)) {
744 return Err(format!(
745 "Cache-transfer layerwise peer failed for room {room}"
746 ));
747 }
748 next_session_check = now + Duration::from_millis(100);
749 }
750 if step_ready(ready_step(address), target) {
751 return Ok(());
752 }
753 thread::sleep(Duration::from_micros(100));
754 }
755 }
756
757 fn transfer_layerwise(
760 &self,
761 room: u64,
762 selection: &Selection,
763 target_steps: &[u64],
764 is_last: bool,
765 ) -> Result<(), String> {
766 let address = self
767 .layerwise
768 .as_ref()
769 .map(|layerwise| layerwise.step_address)
770 .ok_or("Cache-transfer layerwise transfer has no producer step counter")?;
771 let destinations: Vec<Destination> = self
772 .state()
773 .destinations
774 .get(&room)
775 .cloned()
776 .unwrap_or_default()
777 .into_iter()
778 .filter(|destination| !destination.is_dummy())
779 .collect();
780 if destinations.is_empty() {
781 return Err("CachePD transfer room has no destinations".to_owned());
782 }
783 let registrations = self.live_registrations(&destinations)?;
784 let sessions: Vec<String> = destinations.iter().map(|d| d.session_id.clone()).collect();
785 let plans = registrations
787 .iter()
788 .zip(&destinations)
789 .map(|(registration, destination)| {
790 self.plan_copies(
791 registration,
792 Source::Selection(selection),
793 destination,
794 &self.interval_fields,
795 )
796 })
797 .collect::<Result<Vec<_>, _>>()?;
798 for (interval, &target) in target_steps.iter().enumerate() {
799 self.wait_for_step(address, target, room, &sessions)?;
800 for (registration, plan) in registrations.iter().zip(&plans) {
801 let copies = plan
802 .get(interval)
803 .ok_or("a layerwise plan has fewer intervals than the chunk")?;
804 if !copies.is_empty() {
805 self.write_copies(registration, copies)?;
806 }
807 }
808 }
809 tracing::debug!(
810 room,
811 intervals = target_steps.len(),
812 is_last,
813 "layerwise chunk sent"
814 );
815 if !is_last {
816 return Ok(());
817 }
818 let (bootstrap_token, spec_candidate_ids) = loop {
820 let mut state = self.state();
821 if state.ended(room) {
822 return Ok(());
823 }
824 if let Some(metadata) = state.prefill_metadata.remove(&room) {
825 break metadata;
826 }
827 let (_state, _) = self
828 .metadata_ready
829 .wait_timeout(state, Duration::from_millis(100))
830 .expect("the PD state lock is poisoned");
831 };
832 self.report_success(room, ®istrations, bootstrap_token, &spec_candidate_ids);
833 Ok(())
834 }
835
836 fn transfer_full(
838 &self,
839 room: u64,
840 source: Option<&Manifest>,
841 bootstrap_token: i64,
842 spec_candidate_ids: &Option<Vec<i32>>,
843 ) -> Result<(), String> {
844 let destinations = self
845 .state()
846 .destinations
847 .get(&room)
848 .cloned()
849 .filter(|destinations| !destinations.is_empty())
850 .ok_or("CachePD transfer room has no destinations")?;
851 if destinations.iter().any(Destination::is_dummy) {
852 let mut state = self.state();
854 if state.rooms.get(room).is_some() {
855 state.rooms.update(room, TransferPoll::Success);
856 state.destinations.remove(&room);
857 }
858 return Ok(());
859 }
860 let registrations = self.live_registrations(&destinations)?;
861 let source = source.ok_or("Cache-transfer source manifest is unavailable")?;
862 let mut prepared = Vec::with_capacity(destinations.len());
863 for (destination, registration) in destinations.iter().zip(registrations) {
864 let copies = self
865 .plan_copies(
866 ®istration,
867 Source::Manifest(source),
868 destination,
869 &[None],
870 )?
871 .pop()
872 .unwrap_or_default();
873 prepared.push((registration, copies));
874 }
875 for (registration, copies) in &prepared {
876 self.write_copies(registration, copies)?;
877 }
878 let registrations: Vec<Registration> = prepared
879 .into_iter()
880 .map(|(registration, _)| registration)
881 .collect();
882 self.report_success(room, ®istrations, bootstrap_token, spec_candidate_ids);
883 Ok(())
884 }
885
886 fn run_worker(&self, queue: &mpsc::Receiver<Chunk>) {
887 while let Ok(chunk) = queue.recv() {
888 if let Err(reason) = self.transfer(&chunk) {
889 if self.state().ended(chunk.room()) {
890 tracing::debug!(
893 room = chunk.room(),
894 reason,
895 "dropping an ended room's transfer"
896 );
897 continue;
898 }
899 tracing::error!(room = chunk.room(), reason, "CachePD transfer failed");
900 self.abort_room(chunk.room(), &format!("CachePD transfer failed: {reason}"));
901 }
902 }
903 }
904}
905
906#[derive(Clone)]
915pub struct PrefillOutbox {
916 shared: Arc<Shared>,
917 queues: Arc<Vec<mpsc::Sender<Chunk>>>,
918 staged: Staged,
919 armed_steps: Arc<Mutex<u64>>,
922}
923
924impl PrefillOutbox {
925 pub fn release_registration(&self) -> Result<(), String> {
929 let mut sending: Vec<u64> = self.shared.state().tracked.iter().copied().collect();
930 if !sending.is_empty() {
931 sending.sort_unstable();
932 return Err(format!(
933 "cannot release the cache while KV sends are in flight: rooms {sending:?}"
934 ));
935 }
936 self.shared.transport.release_regions()
937 }
938
939 pub fn restore_registration(&self) -> Result<(), String> {
943 self.shared.transport.restore_regions(false)
944 }
945
946 pub fn streams_layerwise(&self) -> bool {
948 self.shared.layerwise.is_some()
949 }
950
951 pub fn stage_remote_decode(&self, rows: BatchRows) -> Result<(), String> {
953 self.staged.stage(rows)
954 }
955
956 pub fn remote_decode_pending(&self) -> bool {
958 self.staged.is_pending()
959 }
960
961 pub fn hand_over_remote_decode(&self) -> Result<(), String> {
966 let rows = self.staged.take()?;
967 for (index, rid) in rows.request_ids.iter().enumerate() {
968 let Some(room) = self.shared.state().rooms_by_rid.get(rid).copied() else {
969 continue;
970 };
971 let token = *rows
972 .decode_input_ids
973 .get(index)
974 .ok_or("a remote decode row has no first token")?;
975 if token < 0 {
976 return Err("Cache-transfer bootstrap token is unavailable".to_owned());
977 }
978 let spec_candidate_ids = rows
979 .spec_candidate_ids
980 .get(index)
981 .filter(|ids| !ids.is_empty())
982 .cloned();
983 let source = {
984 let state = self.shared.state();
985 let destinations = state.destinations.get(&room).map_or(&[][..], Vec::as_slice);
986 if destinations.is_empty() {
987 drop(state);
988 self.send_unplanned(rid, room)?;
989 continue;
990 }
991 let windows: Vec<&Manifest> = destinations
992 .iter()
993 .filter_map(|destination| destination.manifest.as_deref())
994 .collect();
995 match windows.first() {
996 Some(window) if !state.layerwise_rooms.contains(&room) => {
997 same_prompt_window(&windows)?;
998 Some(Arc::new(
999 build_manifest(
1000 &rows.tables,
1001 &self.shared.layout.local,
1002 index,
1003 window.prefix_len,
1004 window.prompt_len,
1005 window.state_prefix_cached,
1006 )
1007 .map_err(|error| error.0)?,
1008 ))
1009 }
1010 _ => None,
1011 }
1012 };
1013 self.send(rid, room, i64::from(token), spec_candidate_ids, source);
1014 }
1015 Ok(())
1016 }
1017
1018 pub fn arm_layerwise(&self, forward: &BatchRows) -> Result<(), String> {
1023 let Some(layerwise) = &self.shared.layerwise else {
1024 return Ok(());
1025 };
1026 if forward.num_extends == 0 {
1027 return Ok(());
1028 }
1029 let chunks = layerwise_chunks(&self.shared.state(), &self.shared.layout.local, forward)?;
1030 let step_count = layerwise.fields_by_step.len() as u64;
1033 let begin = {
1034 let mut armed = self.armed_steps.lock().expect("the step lock is poisoned");
1035 let begin = *armed;
1036 *armed = (begin + step_count) % STEP_COUNT_MAX;
1037 begin
1038 };
1039 let targets = interval_targets(begin, step_count, layerwise.interval.max(1) as u64);
1040 for (rid, room, is_last, selection) in chunks {
1041 self.send_layerwise(&rid, room, Arc::new(selection), targets.clone(), is_last);
1042 }
1043 Ok(())
1044 }
1045
1046 fn send(
1050 &self,
1051 rid: &str,
1052 room: u64,
1053 bootstrap_token: i64,
1054 spec_candidate_ids: Option<Vec<i32>>,
1055 source: Option<Arc<Manifest>>,
1056 ) {
1057 {
1058 let mut state = self.shared.state();
1059 if state.layerwise_rooms.contains(&room) {
1060 if !state.ended(room) {
1062 state
1063 .prefill_metadata
1064 .insert(room, (bootstrap_token, spec_candidate_ids));
1065 self.shared.metadata_ready.notify_all();
1066 }
1067 return;
1068 }
1069 }
1070 self.enqueue(
1071 rid,
1072 Chunk::Full {
1073 room,
1074 source,
1075 bootstrap_token,
1076 spec_candidate_ids,
1077 },
1078 );
1079 }
1080
1081 fn send_layerwise(
1084 &self,
1085 rid: &str,
1086 room: u64,
1087 selection: Arc<Selection>,
1088 target_steps: Vec<u64>,
1089 is_last: bool,
1090 ) {
1091 {
1092 let mut state = self.shared.state();
1093 if state.ended(room) {
1094 return;
1095 }
1096 state.layerwise_rooms.insert(room);
1097 }
1098 self.enqueue(
1099 rid,
1100 Chunk::Layerwise {
1101 room,
1102 selection,
1103 target_steps,
1104 is_last,
1105 },
1106 );
1107 }
1108
1109 fn enqueue(&self, rid: &str, chunk: Chunk) {
1112 let room = chunk.room();
1113 let shard = {
1114 let state = self.shared.state();
1115 if state.ended(room) {
1116 tracing::debug!(rid, room, "the room already failed or ended");
1117 return;
1118 }
1119 let Some(destinations) = state.destinations.get(&room) else {
1120 return;
1122 };
1123 let ports: u64 = destinations
1124 .iter()
1125 .filter_map(|destination| wire::session_port(&destination.session_id))
1126 .sum();
1127 (ports % self.queues.len() as u64) as usize
1128 };
1129 if self.queues[shard].send(chunk).is_err() {
1130 tracing::error!(rid, room, "the PD transfer workers stopped");
1131 }
1132 }
1133
1134 fn send_unplanned(&self, rid: &str, room: u64) -> Result<(), String> {
1138 if self.shared.state().ended(room) {
1139 return Ok(());
1140 }
1141 Err(format!(
1142 "Cache-transfer destination metadata is unavailable for {rid} (room {room})"
1143 ))
1144 }
1145}
1146
1147fn layerwise_chunks(
1154 state: &State,
1155 layout: &Contract,
1156 forward: &BatchRows,
1157) -> Result<Vec<(String, u64, bool, Selection)>, String> {
1158 let mut chunks = Vec::new();
1159 for index in 0..forward.num_extends {
1160 let rid = &forward.request_ids[index];
1161 let Some(&room) = state.rooms_by_rid.get(rid) else {
1162 continue;
1163 };
1164 let windows: Vec<&Manifest> = state
1165 .destinations
1166 .get(&room)
1167 .map_or(&[][..], Vec::as_slice)
1168 .iter()
1169 .filter_map(|destination| destination.manifest.as_deref())
1170 .collect();
1171 let Some(window) = windows.first() else {
1173 continue;
1174 };
1175 same_prompt_window(&windows)?;
1176 let chunk_begin = i64::from(forward.extend_prefix_lens[index]);
1177 let chunk_end = chunk_begin + i64::from(forward.input_lengths[index]);
1178 if chunk_end > window.prompt_len {
1179 return Err(
1180 "Cache-transfer Prefill chunk extends past Decode's prompt manifest".to_owned(),
1181 );
1182 }
1183 let is_last = chunk_end == window.prompt_len;
1184 let mut selection_start = chunk_begin;
1185 if !state.layerwise_rooms.contains(&room) {
1186 selection_start = selection_start.min(window.prefix_len);
1187 }
1188 let selection = build_selection(
1189 &forward.tables,
1190 layout,
1191 index,
1192 window.prefix_len,
1193 window.prompt_len,
1194 selection_start,
1195 chunk_end,
1196 window.state_prefix_cached,
1197 )
1198 .map_err(|error| error.0)?;
1199 if !is_last
1200 && selection
1201 .groups
1202 .iter()
1203 .all(|group| group.source_block_ids.is_empty())
1204 {
1205 continue;
1206 }
1207 chunks.push((rid.clone(), room, is_last, selection));
1208 }
1209 Ok(chunks)
1210}
1211
1212fn interval_targets(begin: u64, step_count: u64, interval: u64) -> Vec<u64> {
1215 (0..step_count)
1216 .step_by(interval as usize)
1217 .map(|first| (begin + (first + interval).min(step_count) - 1) % STEP_COUNT_MAX)
1218 .collect()
1219}
1220
1221fn same_prompt_window(windows: &[&Manifest]) -> Result<(), String> {
1223 let Some(first) = windows.first() else {
1224 return Ok(());
1225 };
1226 if windows.iter().any(|other| {
1227 other.prefix_len != first.prefix_len
1228 || other.prompt_len != first.prompt_len
1229 || other.state_prefix_cached != first.state_prefix_cached
1230 }) {
1231 return Err("Cache-transfer destinations disagree on the prompt window".to_owned());
1232 }
1233 Ok(())
1234}
1235
1236struct Sender {
1238 room: u64,
1239 started: Instant,
1240 local: TransferPoll,
1241 concluded: Option<TransferPoll>,
1242}
1243
1244pub struct PrefillPeer {
1246 shared: Arc<Shared>,
1247 stop: Arc<AtomicBool>,
1248 bootstrap_timeout: Duration,
1249 order: Vec<String>,
1252 senders: HashMap<String, Sender>,
1253 stall_logged_at: Option<Instant>,
1254 timeout_logged: bool,
1255}
1256
1257impl PrefillPeer {
1258 pub fn start(config: PrefillConfig) -> Result<(Self, PrefillOutbox), StartError> {
1261 let (incoming, messages) = mpsc::channel();
1262 let transport = Transport::start(&config.peer, incoming)?;
1263 let port = transport.sockets.port();
1264 let interval_fields = config
1265 .layerwise
1266 .as_ref()
1267 .map(Layerwise::interval_fields)
1268 .unwrap_or_default();
1269 let shared = Arc::new(Shared {
1270 transport,
1271 layout: config.layout,
1272 state: Mutex::new(State::default()),
1273 status_rank: config.status_rank,
1274 failed_session_ttl: config.failed_session_ttl,
1275 metrics: config.peer.metrics.clone(),
1276 layerwise: config.layerwise,
1277 interval_fields,
1278 metadata_ready: Condvar::new(),
1279 });
1280 let stop = Arc::new(AtomicBool::new(false));
1281 {
1282 let shared = shared.clone();
1283 let stop = stop.clone();
1284 let unclaimed_ttl = config.bootstrap_timeout;
1285 thread::Builder::new()
1286 .name("ts-pd-bootstrap".to_owned())
1287 .spawn(move || {
1288 let mut swept = Instant::now();
1289 loop {
1290 match messages.recv_timeout(SOCKET_POLL) {
1291 Ok(frames) => shared.handle(&frames),
1292 Err(mpsc::RecvTimeoutError::Timeout) => {
1293 if stop.load(Ordering::Relaxed) {
1294 return;
1295 }
1296 }
1297 Err(mpsc::RecvTimeoutError::Disconnected) => return,
1298 }
1299 if swept.elapsed() >= SOCKET_POLL {
1300 swept = Instant::now();
1301 shared.drop_unclaimed(unclaimed_ttl);
1302 }
1303 }
1304 })?;
1305 }
1306 let mut queues = Vec::with_capacity(config.transfer_workers.max(1));
1307 for index in 0..config.transfer_workers.max(1) {
1308 let (queue, chunks) = mpsc::channel();
1309 let shared = shared.clone();
1310 thread::Builder::new()
1311 .name(format!("ts-pd-transfer-{index}"))
1312 .spawn(move || shared.run_worker(&chunks))?;
1313 queues.push(queue);
1314 }
1315
1316 let mut payload = config.bootstrap_payload.clone();
1317 payload["rank_port"] = port.into();
1318 match http::put_json(&config.bootstrap_url, &payload, Duration::from_secs(5)) {
1319 Ok(()) => tracing::debug!("registered with the bootstrap server"),
1320 Err(error) => tracing::error!(
1321 url = config.bootstrap_url,
1322 error,
1323 "the Prefill rank failed to register with the bootstrap server"
1324 ),
1325 }
1326 let outbox = PrefillOutbox {
1327 shared: shared.clone(),
1328 queues: Arc::new(queues),
1329 staged: Staged::default(),
1330 armed_steps: Arc::new(Mutex::new(0)),
1331 };
1332 Ok((
1333 Self {
1334 shared,
1335 stop,
1336 bootstrap_timeout: config.bootstrap_timeout,
1337 order: Vec::new(),
1338 senders: HashMap::new(),
1339 stall_logged_at: None,
1340 timeout_logged: false,
1341 },
1342 outbox,
1343 ))
1344 }
1345
1346 pub fn port(&self) -> u16 {
1348 self.shared.transport.sockets.port()
1349 }
1350
1351 pub fn embedding_receiver(
1354 &self,
1355 config: crate::epd::receiver::ReceiverConfig,
1356 ) -> Result<crate::epd::receiver::Receiver, StartError> {
1357 crate::epd::receiver::Receiver::start(config, self.shared.transport.clone())
1358 }
1359
1360 pub fn session_id(&self) -> String {
1362 self.shared.transport.session_id()
1363 }
1364
1365 fn log_stalls(&mut self, polls: &[i64]) {
1366 let now = Instant::now();
1367 if self
1368 .stall_logged_at
1369 .is_some_and(|at| now - at < BOOTSTRAP_STALL)
1370 {
1371 return;
1372 }
1373 let stalled: Vec<String> = self
1374 .order
1375 .iter()
1376 .zip(polls)
1377 .filter(|(_, &poll)| poll == TransferPoll::Bootstrapping as i64)
1378 .filter_map(|(rid, _)| {
1379 let sender = &self.senders[rid];
1380 let age = now - sender.started;
1381 (age >= BOOTSTRAP_STALL)
1382 .then(|| format!("{rid}(room={}, age={:.0}s)", sender.room, age.as_secs_f64()))
1383 })
1384 .take(8)
1385 .collect();
1386 if !stalled.is_empty() {
1387 self.stall_logged_at = Some(now);
1388 tracing::warn!(?stalled, "requests still bootstrapping");
1389 }
1390 }
1391
1392 fn forget(&mut self, rid: &str) {
1393 let Some(sender) = self.senders.remove(rid) else {
1394 return;
1395 };
1396 self.order.retain(|tracked| tracked != rid);
1397 let mut state = self.shared.state();
1398 state.clear(sender.room);
1399 state.rooms_by_rid.remove(rid);
1400 }
1401}
1402
1403impl Drop for PrefillPeer {
1404 fn drop(&mut self) {
1405 self.stop.store(true, Ordering::Relaxed);
1406 }
1407}
1408
1409impl TransferPeer for PrefillPeer {
1410 fn register(&mut self, rid: &str, bootstrap: &BootstrapInfo) -> Result<(), PdError> {
1411 let room = bootstrap.room;
1412 {
1413 let mut state = self.shared.state();
1414 state.cached_tokens.remove(&room);
1415 state.bootstrap_logprobs.remove(&room);
1416 state.rooms.update(room, TransferPoll::Bootstrapping);
1417 state.unclaimed.remove(&room);
1420 state.tracked.insert(room);
1421 state.rooms_by_rid.insert(rid.to_owned(), room);
1422 }
1423 if self
1424 .senders
1425 .insert(
1426 rid.to_owned(),
1427 Sender {
1428 room,
1429 started: Instant::now(),
1430 local: TransferPoll::Bootstrapping,
1431 concluded: None,
1432 },
1433 )
1434 .is_none()
1435 {
1436 self.order.push(rid.to_owned());
1437 }
1438 Ok(())
1439 }
1440
1441 fn poll(&mut self, mesh: &mut Mesh, replica: &Group) -> Result<Vec<PdEvent>, PdError> {
1442 if self.order.is_empty() {
1443 return Ok(Vec::new());
1444 }
1445 let now = Instant::now();
1446 let mut polls = Vec::with_capacity(self.order.len());
1447 {
1448 let mut state = self.shared.state();
1449 for rid in &self.order {
1450 let sender = self.senders.get_mut(rid).expect("tracked");
1451 let poll = match sender.concluded {
1452 Some(poll) => poll,
1453 None => match state.rooms.get(sender.room).unwrap_or(TransferPoll::Failed) {
1454 status @ (TransferPoll::Success | TransferPoll::Failed) => {
1455 sender.concluded = Some(status);
1456 status
1457 }
1458 TransferPoll::Bootstrapping
1459 if now - sender.started >= self.bootstrap_timeout =>
1460 {
1461 let elapsed = (now - sender.started).as_secs_f64();
1462 if !self.timeout_logged {
1463 self.timeout_logged = true;
1464 tracing::warn!(
1465 "Some requests timed out when bootstrapping: the decode \
1466 instance never sent their cache manifest. Raise \
1467 TOKENSPEED_DISAGGREGATION_BOOTSTRAP_TIMEOUT if a longer \
1468 TTFT is acceptable."
1469 );
1470 }
1471 state.rooms.record_failure(
1472 sender.room,
1473 format!(
1474 "Request {} timed out after {elapsed:.1}s in \
1475 TransferPoll.Bootstrapping",
1476 sender.room
1477 ),
1478 );
1479 if let Some(metrics) = &self.shared.metrics {
1480 metrics.record_timeout("bootstrapping");
1481 }
1482 sender.concluded = Some(TransferPoll::Failed);
1483 TransferPoll::Failed
1484 }
1485 status => status,
1486 },
1487 };
1488 polls.push(poll as i64);
1489 }
1490 }
1491 let agreed = if replica.size() > 1 {
1492 mesh.all_reduce(replica, &polls, Reduce::Min)?
1493 } else {
1494 polls
1495 };
1496 self.log_stalls(&agreed);
1497
1498 let mut events = Vec::new();
1499 let mut ended = Vec::new();
1500 for (rid, &poll) in self.order.iter().zip(&agreed) {
1501 let sender = self.senders.get_mut(rid).expect("tracked");
1502 let poll = TransferPoll::from_wire(poll).unwrap_or(TransferPoll::Failed);
1503 if sender.local == TransferPoll::Bootstrapping && poll == TransferPoll::Bootstrapped {
1504 sender.local = TransferPoll::Bootstrapped;
1505 events.push(PdEvent::Bootstrapped(rid.clone()));
1506 } else if poll == TransferPoll::Failed {
1507 let reason = self
1508 .shared
1509 .state()
1510 .rooms
1511 .failure(sender.room)
1512 .map(str::to_owned);
1513 tracing::warn!(rid, room = sender.room, reason, "KV transfer failed");
1514 events.push(PdEvent::Failed(rid.clone()));
1515 ended.push(rid.clone());
1516 } else if sender.local == TransferPoll::Bootstrapped && poll == TransferPoll::Success {
1517 sender.local = TransferPoll::Success;
1518 events.push(PdEvent::Succeeded(rid.clone()));
1519 ended.push(rid.clone());
1520 }
1521 }
1522 for rid in ended {
1523 self.forget(&rid);
1524 }
1525 Ok(events)
1526 }
1527
1528 fn has_in_flight(&self) -> bool {
1529 !self.order.is_empty()
1530 }
1531
1532 fn abort_room(&mut self, rid: &str, room: u64) {
1533 self.shared.abort_room(
1534 room,
1535 &format!("EPD: prefill aborted request {rid} (embedding receive timed out)"),
1536 );
1537 }
1538
1539 fn record_usage(&mut self, rid: &str, cached_tokens: i64, bootstrap_logprob: Option<f64>) {
1540 let Some(sender) = self.senders.get(rid) else {
1541 return;
1542 };
1543 let mut state = self.shared.state();
1544 if state.ended(sender.room) {
1545 return;
1546 }
1547 state.cached_tokens.insert(sender.room, cached_tokens);
1548 if let Some(logprob) = bootstrap_logprob {
1549 state.bootstrap_logprobs.insert(sender.room, logprob);
1550 }
1551 }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556 use super::*;
1557
1558 #[test]
1559 fn step_readiness_is_strict_and_survives_the_wrap() {
1560 assert!(!step_ready(0, 0));
1561 assert!(!step_ready(STEP_COUNT_MAX - 1, 0));
1562 assert!(step_ready(1, 0));
1563 assert!(step_ready(0, STEP_COUNT_MAX - 1));
1564 }
1565
1566 #[test]
1567 fn unclaimed_rooms_expire_and_their_decode_ranks_are_told() {
1568 let mut state = State::default();
1569 let layout =
1570 Contract::from_wire(crate::layout::contract::tests::wire().as_bytes()).unwrap();
1571 state.registrations.insert(
1572 "s0".to_owned(),
1573 Registration {
1574 endpoint: "decode".to_owned(),
1575 port: 7,
1576 session_id: "s0".to_owned(),
1577 decode_tp_size: 1,
1578 decode_tp_rank: 0,
1579 arena_base: 0x1000,
1580 layout: Arc::new(layout),
1581 planned: Arc::new(PlannedRegistration {
1582 fragments: Vec::new(),
1583 owner_filters: Default::default(),
1584 is_dummy: false,
1585 expected_decode_ranks: BTreeSet::from([0]),
1586 }),
1587 },
1588 );
1589 let destination = || {
1590 vec![Destination {
1591 session_id: "s0".to_owned(),
1592 manifest: Some(Arc::new(Manifest {
1593 groups: Vec::new(),
1594 prefix_len: 0,
1595 prompt_len: 1,
1596 state_prefix_cached: false,
1597 })),
1598 }]
1599 };
1600 state.destinations.insert(1, destination());
1602 state.touch(1);
1603 state.rooms.update(2, TransferPoll::Failed);
1606 state.touch(2);
1607 state.tracked.insert(3);
1609 state.destinations.insert(3, destination());
1610 state.touch(3);
1611
1612 assert_eq!(
1613 state.take_unclaimed(Duration::from_secs(60)),
1614 (Vec::new(), Vec::new())
1615 );
1616 let (expired, notices) = state.take_unclaimed(Duration::ZERO);
1617 assert_eq!(expired, [1, 2]);
1618 assert_eq!(notices, [(("decode".to_owned(), 7), 1)]);
1619 assert!(state.unclaimed.is_empty());
1620 assert!(!state.destinations.contains_key(&1));
1621 assert_eq!(state.rooms.get(2), None);
1622 assert!(state.destinations.contains_key(&3));
1623 }
1624
1625 fn grouped_layout() -> Contract {
1627 let group = |id: &str| serde_json::json!({"group_id": id, "cache_blocks_per_lcm_block": 1, "page_count": 32});
1628 let field = |group: &str, field: &str, offset: i64| {
1629 serde_json::json!({"group_id": group, "field_id": field, "plane_id": "p0",
1630 "shape": [4], "dtype": "bfloat16", "field_offset_bytes": offset,
1631 "page_stride_bytes": 32})
1632 };
1633 let state = |id: &str| {
1634 serde_json::json!({"group_id": id, "attn_kind": "state", "replayable": false,
1635 "transfer_policy": "latest_snapshot", "shard_count": 1})
1636 };
1637 let wire = serde_json::json!({
1638 "plan": {
1639 "prefix_granularity": 2,
1640 "lcm_block_bytes": 32,
1641 "num_lcm_blocks": 31,
1642 "groups": [group("history"), group("state-a"), group("state-b")],
1643 "planes": [{"plane_id": "p0", "bytes_per_lcm_block": 32, "arena_offset_bytes": 0}],
1644 "fields": [field("history", "layer.0.kv", 0), field("state-a", "layer.2.state", 8),
1645 field("state-b", "layer.4.state", 16)],
1646 "resident_block_bytes": null
1647 },
1648 "group_specs": [
1649 {"group_id": "history", "attn_kind": "full_history", "rows_per_page": 2,
1650 "entry_stride_tokens": 1, "replayable": false, "transfer_policy": "full_suffix",
1651 "shard_count": 1},
1652 state("state-a"),
1653 state("state-b")
1654 ],
1655 "transfer_schema": {"fields": []},
1656 "field_addresses": null
1657 });
1658 Contract::from_wire(wire.to_string().as_bytes()).unwrap()
1659 }
1660
1661 fn destined(state: &mut State, rid: &str, room: u64, prefix: i64, prompt: i64) {
1664 state.rooms_by_rid.insert(rid.to_owned(), room);
1665 state.destinations.insert(
1666 room,
1667 vec![Destination {
1668 session_id: "decode:7000".to_owned(),
1669 manifest: Some(Arc::new(Manifest {
1670 groups: Vec::new(),
1671 prefix_len: prefix,
1672 prompt_len: prompt,
1673 state_prefix_cached: false,
1674 })),
1675 }],
1676 );
1677 }
1678
1679 fn chunk_forward(rows: &[(&str, i32, i32)], tables: &[(&str, Vec<Vec<i64>>)]) -> BatchRows {
1680 BatchRows {
1681 request_ids: rows.iter().map(|row| row.0.to_owned()).collect(),
1682 extend_prefix_lens: rows.iter().map(|row| row.1).collect(),
1683 input_lengths: rows.iter().map(|row| row.2).collect(),
1684 num_extends: rows.len(),
1685 tables: tables
1686 .iter()
1687 .map(|(group, rows)| {
1688 let columns = rows[0].len();
1689 (
1690 (*group).to_owned(),
1691 crate::layout::manifest::BlockTable::from_rows(rows, columns),
1692 )
1693 })
1694 .collect(),
1695 ..BatchRows::default()
1696 }
1697 }
1698
1699 #[test]
1700 fn a_forward_arms_a_chunk_per_destined_row_only_when_every_row_plans() {
1701 let layout = grouped_layout();
1702 let mut state = State::default();
1703 destined(&mut state, "request-0", 9, 0, 4);
1704 destined(&mut state, "request-1", 10, 0, 4);
1705 let mut tables = vec![
1706 ("history", vec![vec![1, 2], vec![3, 4]]),
1707 ("state-a", vec![vec![5, 6], vec![7, 8]]),
1708 ("state-b", vec![vec![9, 10], vec![11, 12]]),
1709 ];
1710 let rows = [
1711 ("request-0", 0, 4),
1712 ("request-1", 0, 4),
1713 ("untracked", 0, 4),
1714 ];
1715 let chunks = layerwise_chunks(&state, &layout, &chunk_forward(&rows, &tables)).unwrap();
1716 let armed: Vec<(&str, u64, bool, Vec<i64>)> = chunks
1717 .iter()
1718 .map(|(rid, room, is_last, selection)| {
1719 (
1720 rid.as_str(),
1721 *room,
1722 *is_last,
1723 selection.groups[1].source_block_ids.clone(),
1724 )
1725 })
1726 .collect();
1727 assert_eq!(
1729 armed,
1730 [
1731 ("request-0", 9, true, vec![6]),
1732 ("request-1", 10, true, vec![8])
1733 ]
1734 );
1735 tables[0].1[1][0] = 0;
1737 let error = layerwise_chunks(&state, &layout, &chunk_forward(&rows, &tables)).unwrap_err();
1738 assert!(error.contains("invalid block ID"), "{error}");
1739 }
1740
1741 #[test]
1742 fn a_rooms_first_chunk_carries_a_deeper_prefill_cache_hit() {
1743 let layout = grouped_layout();
1744 let mut state = State::default();
1745 destined(&mut state, "request-0", 9, 2, 9);
1747 let tables = [
1748 ("history", vec![vec![1, 2, 3, 4, 5]]),
1749 ("state-a", vec![vec![6, 7]]),
1750 ("state-b", vec![vec![11, 12]]),
1751 ];
1752 let history = |state: &State, begin, length| {
1753 let forward = chunk_forward(&[("request-0", begin, length)], &tables);
1754 let (_, _, is_last, selection) = layerwise_chunks(state, &layout, &forward)
1755 .unwrap()
1756 .pop()
1757 .unwrap();
1758 let group = selection.groups[0].clone();
1759 (is_last, group.source_block_ids, group.destination_positions)
1760 };
1761 assert_eq!(history(&state, 4, 3), (false, vec![2, 3], vec![0, 1]));
1764 state.layerwise_rooms.insert(9);
1765 assert_eq!(history(&state, 7, 2), (true, vec![4, 5], vec![2, 3]));
1767 }
1768
1769 #[test]
1770 fn each_interval_completes_at_its_last_producer_step() {
1771 assert_eq!(interval_targets(0, 5, 2), [1, 3, 4]);
1772 assert_eq!(interval_targets(0, 3, 1), [0, 1, 2]);
1774 assert_eq!(interval_targets(3, 3, 1), [3, 4, 5]);
1775 assert_eq!(
1776 interval_targets(STEP_COUNT_MAX - 1, 2, 1),
1777 [STEP_COUNT_MAX - 1, 0]
1778 );
1779 }
1780}