Skip to main content

ts_pd/
prefill.rs

1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! A Prefill rank's peer: Decode ranks register with it and pre-allocate
22//! each request's pages; once the prompt's last chunk commits, a transfer
23//! worker writes its KV and reports to every destination.
24//!
25//! Threads: one handles the control socket (registrations and
26//! pre-allocations), a few run transfers, sharded by destination session so
27//! one session's transfers stay ordered. The scheduling loop only registers
28//! requests, records usage and polls; the forward thread hands over each
29//! plan's remote decode and arms layerwise chunks ([`PrefillOutbox`]).
30//!
31//! Every route, manifest and copy is planned here, over the rank's cache
32//! transfer contract ([`crate::layout`]).
33
34use 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
64/// A request still bootstrapping this long after registration is reported.
65const BOOTSTRAP_STALL: Duration = Duration::from_secs(10);
66
67/// How often an idle control-socket thread checks for shutdown.
68const SOCKET_POLL: Duration = Duration::from_millis(100);
69
70/// A Decode rank's planned registration with this rank.
71#[derive(Debug, Clone, PartialEq)]
72struct Registration {
73    /// Where the Decode rank's control socket listens.
74    endpoint: String,
75    /// Its port.
76    port: u16,
77    /// Its transfer session.
78    session_id: String,
79    /// The Decode node's attention TP size.
80    decode_tp_size: i64,
81    /// The Decode rank's attention TP rank.
82    decode_tp_rank: i64,
83    /// Its arena base.
84    arena_base: u64,
85    /// Its contract, with its landing addresses.
86    layout: Arc<Contract>,
87    /// The route this rank planned to it.
88    planned: Arc<PlannedRegistration>,
89}
90
91impl Registration {
92    fn is_dummy(&self) -> bool {
93        self.planned.is_dummy
94    }
95}
96
97/// Layerwise streaming: each prefill chunk's KV goes out an interval of
98/// producer steps at a time, as the forward writes it.
99#[derive(Debug, Clone)]
100pub struct Layerwise {
101    /// The pinned host word the device updates with the last producer step
102    /// whose KV landed (`StepCounter`).
103    pub step_address: usize,
104    /// The fields each producer step makes ready, in step order
105    /// (`CacheProducerSchedule`).
106    pub fields_by_step: Vec<Vec<String>>,
107    /// Producer steps per transfer interval.
108    pub interval: usize,
109}
110
111impl Layerwise {
112    /// Each transfer interval's fields.
113    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/// A Prefill rank's settings.
123#[derive(Debug, Clone)]
124pub struct PrefillConfig {
125    /// What both roles share.
126    pub peer: PeerConfig,
127    /// What this rank plans its routes and copies with.
128    pub layout: PrefillLayout,
129    /// This rank in Decode's completion-tracking rank space.
130    pub status_rank: i64,
131    /// The bootstrap server's registration route.
132    pub bootstrap_url: String,
133    /// What this rank registers there, but its control port.
134    pub bootstrap_payload: serde_json::Value,
135    /// A request whose Decode ranks have not all pre-allocated this long
136    /// after registration fails.
137    pub bootstrap_timeout: Duration,
138    /// After a transfer to a session fails, its later transfers fail fast
139    /// for this long; zero disables.
140    pub failed_session_ttl: Duration,
141    /// Transfer workers.
142    pub transfer_workers: usize,
143    /// Layerwise streaming, when on.
144    pub layerwise: Option<Layerwise>,
145}
146
147/// One destination of a room.
148#[derive(Debug, Clone)]
149struct Destination {
150    session_id: String,
151    /// Its manifest of the request's blocks; `None` from a rank this one
152    /// only joins the status agreement with.
153    manifest: Option<Arc<Manifest>>,
154}
155
156impl Destination {
157    fn is_dummy(&self) -> bool {
158        self.manifest.is_none()
159    }
160}
161
162/// Work for a transfer worker.
163enum Chunk {
164    /// A room's whole KV is written: send it.
165    Full {
166        room: u64,
167        /// This rank's manifest of the request's blocks; `None` when every
168        /// destination is a dummy.
169        source: Option<Arc<Manifest>>,
170        bootstrap_token: i64,
171        spec_candidate_ids: Option<Vec<i32>>,
172    },
173    /// Layerwise: one prefill chunk's KV, sent an interval at a time as the
174    /// forward writes it; the last chunk then reports the room done, once
175    /// its bootstrap payload arrived.
176    Layerwise {
177        room: u64,
178        /// The blocks the chunk makes ready.
179        selection: Arc<Selection>,
180        /// The producer step each interval's KV is complete at.
181        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
194/// Producer steps count modulo this (`StepCounter.COUNT_NUM_MAX`).
195const STEP_COUNT_MAX: u64 = 1 << 62;
196
197/// The counter at `current` is strictly past `target`, across the counter's
198/// wrap: a step more than half the range ahead counts as behind.
199fn 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/// The last landed producer step: a plain load of the data plane's pinned
204/// host word, which the device updates asynchronously.
205#[allow(unsafe_code)]
206fn ready_step(address: usize) -> u64 {
207    // SAFETY: `address` is the pinned host tensor the data plane allocated
208    // for the engine's lifetime; an aligned 8-byte load of it is atomic.
209    unsafe { std::ptr::read_volatile(address as *const i64) as u64 }
210}
211
212#[derive(Default)]
213struct State {
214    rooms: RoomTable,
215    /// The rooms of the requests the scheduling loop tracks: a sleep may
216    /// not release the cache while one may still send.
217    tracked: HashSet<u64>,
218    /// The tracked requests' rooms, by request.
219    rooms_by_rid: HashMap<String, u64>,
220    /// Rooms no request here has claimed, since when: a Decode rank
221    /// pre-allocated for one before (or without) its request registering
222    /// here, or this rank failed one first (an EPD embedding timeout). One
223    /// still unclaimed after the bootstrap timeout is dropped.
224    unclaimed: HashMap<u64, Instant>,
225    registrations: HashMap<String, Registration>,
226    /// Sessions whose registration was rejected: their reply route.
227    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    /// Rooms whose KV streams layerwise.
233    layerwise_rooms: HashSet<u64>,
234    /// Layerwise rooms' bootstrap payloads, from their remote decode.
235    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    /// `room` gained state: start its unclaimed clock unless a request here
251    /// tracks it.
252    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    /// Forget the rooms unclaimed for `ttl` or longer. Returns them, and the
259    /// failure notices their pre-allocating Decode ranks are owed: a room
260    /// this rank failed already told them.
261    #[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    /// Forget everything kept for `room`.
288    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    /// One complete, unique Decode-rank set.
300    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    /// Layerwise: each transfer interval's fields.
340    interval_fields: Vec<Option<BTreeSet<String>>>,
341    /// Signaled when a layerwise room's bootstrap payload arrives.
342    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    /// Fail a room and tell every destination that pre-allocated for it
364    /// (`abort_room`).
365    fn abort_room(&self, room: u64, reason: &str) {
366        let routes: Vec<_> = {
367            let mut state = self.state();
368            // Once per room: a room's later chunks fail here again.
369            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    /// Plan the route a Decode rank's registration asks of this rank.
408    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    /// Invalidate a session and fail the rooms that reference it.
451    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, &registration.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        // A transfer worker may have failed the room since the check above.
540        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    /// Drop the rooms no request here claimed within `ttl`, telling each
554    /// Decode rank that pre-allocated for one that it failed.
555    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    /// Run one chunk of transfer work.
599    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    /// The copies that move `source` to one Decode rank, per entry of
622    /// `steps` (the fields each moves; `None`: every field).
623    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: &registration.layout,
640            source,
641            destination: manifest,
642            fragments: &registration.planned.fragments,
643            owner_filters: &registration.planned.owner_filters,
644        }
645        .copies(steps)
646        .map_err(|error| error.0)
647    }
648
649    /// The live destinations' registrations, failing if one's session failed.
650    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(&registration.session_id, copies) {
677            self.mark_session_failed(&registration.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    /// Tell every destination the room's KV arrived, with its bootstrap
690    /// payload, and mark it done.
691    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    /// Wait until the producer step `target` landed, failing if the room ends or a
726    /// destination's session fails first.
727    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    /// Send one layerwise chunk an interval at a time; the last one reports
758    /// the room done once its bootstrap payload arrived.
759    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        // Every peer's copies of every interval, planned once, before any DMA.
786        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        // The final chunk's bootstrap payload comes with the remote decode.
819        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, &registrations, bootstrap_token, &spec_candidate_ids);
833        Ok(())
834    }
835
836    /// Write one room's KV to every destination and report.
837    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            // This rank only joins the status agreement for the room.
853            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                    &registration,
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, &registrations, 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                    // Another thread failed or dropped the room after it was
891                    // queued: failing it again would replace its reason.
892                    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/// Where the forward thread hands a Prefill rank's peer its work; never
907/// waits for a transfer.
908///
909/// A plan's remote decode is staged when the plan goes to the data plane
910/// ([`PrefillOutbox::stage_remote_decode`]) and handed over by the data plane
911/// once the drafter's pending round finished
912/// ([`PrefillOutbox::hand_over_remote_decode`]): the KV it sends includes
913/// what that round writes.
914#[derive(Clone)]
915pub struct PrefillOutbox {
916    shared: Arc<Shared>,
917    queues: Arc<Vec<mpsc::Sender<Chunk>>>,
918    staged: Staged,
919    /// Layerwise: the producer steps armed so far, modulo `STEP_COUNT_MAX`
920    /// (the host half of `StepCounter`).
921    armed_steps: Arc<Mutex<u64>>,
922}
923
924impl PrefillOutbox {
925    /// A sleep releases the cache: deregister it from the transfer engine,
926    /// refused while a tracked request's KV may still go out. A room no
927    /// request here claimed sends nothing, so it does not hold the release.
928    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    /// A wake re-mapped the cache: register it again, on the same session
940    /// (every write starts from this rank's own registration, so no peer
941    /// holds its keys).
942    pub fn restore_registration(&self) -> Result<(), String> {
943        self.shared.transport.restore_regions(false)
944    }
945
946    /// Whether prefill chunks stream their KV layerwise.
947    pub fn streams_layerwise(&self) -> bool {
948        self.shared.layerwise.is_some()
949    }
950
951    /// Stage a plan's remote decode: the prompts whose KV goes out now.
952    pub fn stage_remote_decode(&self, rows: BatchRows) -> Result<(), String> {
953        self.staged.stage(rows)
954    }
955
956    /// Whether a staged remote decode still waits to be handed over.
957    pub fn remote_decode_pending(&self) -> bool {
958        self.staged.is_pending()
959    }
960
961    /// The staged remote decode's device prerequisites are met: send each
962    /// prompt's KV to its destinations (`HostPrefillPeer.execute`). A room
963    /// that streams layerwise already sends; it only gets its bootstrap
964    /// payload.
965    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    /// Layerwise streaming: arm one chunk per extend row of the forward the
1019    /// plan launches whose request has a destination, ahead of the forward
1020    /// (`HostPrefillPeer.prepare_prefill`). Every row is planned before any
1021    /// is armed, and the forward's producer steps are counted once.
1022    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        // The attention backends record one producer range per forward, even
1031        // when its chunk completes no block.
1032        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    /// A request's KV is written: send it to its destinations
1047    /// (`add_transfer_request`), or, for a room streaming layerwise, give its
1048    /// final chunk the bootstrap payload.
1049    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                // Its KV already streams: only its final report waits on this.
1061                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    /// Layerwise streaming: queue one armed chunk of `room`'s request; its
1082    /// intervals are complete at `target_steps`.
1083    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    /// Queue `chunk` on the worker its destinations shard to: one room's
1110    /// chunks always reach the same worker, in order.
1111    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                // A dummy rank already succeeded, or the room was dropped.
1121                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    /// A request reached its remote decode with no destination on record:
1135    /// fine for a room that already failed or ended, a broken invariant
1136    /// otherwise.
1137    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
1147/// One forward's layerwise chunks: for each extend row whose request has a
1148/// destination, the blocks its chunk makes ready and whether it ends the
1149/// prompt. A room's first chunk also carries the blocks this rank found in
1150/// its cache that the Decode rank still needs; later ones start at their
1151/// own boundary, so nothing goes twice. A chunk that completes no block is
1152/// skipped, but for the prompt's last.
1153fn 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        // A dummy-only room completes after the forward like any other.
1172        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
1212/// The producer step each transfer interval of a forward is complete at,
1213/// for a forward of `step_count` steps after the first `begin`: its last.
1214fn 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
1221/// Every destination of a room describes the same prompt window.
1222fn 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
1236/// One request's transfer as the scheduling loop tracks it.
1237struct Sender {
1238    room: u64,
1239    started: Instant,
1240    local: TransferPoll,
1241    concluded: Option<TransferPoll>,
1242}
1243
1244/// A Prefill rank's PD transfer peer.
1245pub struct PrefillPeer {
1246    shared: Arc<Shared>,
1247    stop: Arc<AtomicBool>,
1248    bootstrap_timeout: Duration,
1249    /// Tracked requests, in registration order (every replica member
1250    /// registers the same requests in the same order).
1251    order: Vec<String>,
1252    senders: HashMap<String, Sender>,
1253    stall_logged_at: Option<Instant>,
1254    timeout_logged: bool,
1255}
1256
1257impl PrefillPeer {
1258    /// Start the transfer engine, register the cache, open the control
1259    /// socket, start the threads and register with the bootstrap server.
1260    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    /// The control socket's port.
1347    pub fn port(&self) -> u16 {
1348        self.shared.transport.sockets.port()
1349    }
1350
1351    /// EPD, the first stage of a multimodal Prefill node: a receiver of the
1352    /// encode nodes' embeddings, over this peer's transfer engine.
1353    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    /// The transfer engine's session id.
1361    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            // Claimed: its pre-allocations, or its failure, are this
1418            // request's now.
1419            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        // Pre-allocated by a Decode rank; its request never registers here.
1601        state.destinations.insert(1, destination());
1602        state.touch(1);
1603        // Failed here before any request claimed it (an EPD timeout): its
1604        // destinations were told then.
1605        state.rooms.update(2, TransferPoll::Failed);
1606        state.touch(2);
1607        // A request's room never expires.
1608        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    /// A history group of 2-token blocks and two state groups.
1626    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    /// `rid`'s room `room`, pre-allocated by one Decode rank whose manifest
1662    /// spans `prefix..prompt`.
1663    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        // The whole prompt in one chunk: the state groups' live slots go too.
1728        assert_eq!(
1729            armed,
1730            [
1731                ("request-0", 9, true, vec![6]),
1732                ("request-1", 10, true, vec![8])
1733            ]
1734        );
1735        // A later bad row refuses the whole forward.
1736        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        // Decode holds 2 tokens of the 9-token prompt; this rank's cache 4.
1746        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        // The first chunk, [4, 7), also sends the history blocks of [2, 4)
1762        // Decode lacks.
1763        assert_eq!(history(&state, 4, 3), (false, vec![2, 3], vec![0, 1]));
1764        state.layerwise_rooms.insert(9);
1765        // The last starts at its own boundary.
1766        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        // Every forward counts its whole range: the next starts after it.
1773        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}