kvbm_engine/leader/instance.rs
1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5
6use ::velo::Messenger;
7use anyhow::Result;
8use dashmap::DashMap;
9use tokio::sync::{Mutex, mpsc, watch};
10use uuid::Uuid;
11
12use std::sync::Arc;
13
14use crate::{
15 BlockId, G2, G3, InstanceId, SequenceHash, object::ObjectBlockOps, worker::RemoteDescriptor,
16};
17use kvbm_common::LogicalLayoutHandle;
18use kvbm_logical::{
19 blocks::{BlockRegistry, ImmutableBlock},
20 manager::BlockManager,
21};
22use kvbm_physical::transfer::{TransferCompleteNotification, TransferOptions};
23
24use kvbm_physical::manager::{LayoutHandle, SerializedLayout};
25
26use super::{
27 super::worker::Worker,
28 super::worker::group::{ParallelWorkers, SpmdParallelWorkers},
29 AsyncSessionResult,
30 FindMatchesOptions,
31 FindMatchesResult,
32 Leader,
33 OnboardingStatus,
34 ReadyResult,
35 // Legacy SessionHandle for deferred operations
36 SessionHandle as LegacySessionHandle,
37 SessionId,
38 StagingMode,
39 accessor::{BlockAccessor, PolicyContext},
40 session::{
41 BlockHolder, ControlRole, ControllableSessionOptions, ControllableSessionResult,
42 InitiatorSession, MessageTransport, OnboardMessage, OnboardSessionTx, ResponderSession,
43 ServerSession, ServerSessionHandle, ServerSessionOptions, SessionHandle, SessionMessage,
44 SessionMessageTx, SessionPhase, create_server_session, session_handle_state_channel,
45 session_message_channel,
46 },
47 velo::{ExportMetadataCallback, VeloLeaderService},
48};
49
50/// Primary leader implementation for the distributed KVBM system.
51///
52/// `InstanceLeader` coordinates block onboarding across local and remote
53/// instances. It owns a G2 (host memory) `BlockManager` and an optional G3
54/// (disk) `BlockManager`, a set of workers for executing physical transfers,
55/// and a parallel worker abstraction for multi-rank RDMA operations.
56///
57/// Key responsibilities:
58/// - **Block matching**: finding which requested sequence hashes are already
59/// cached locally (via `BlockAccessor` policies).
60/// - **Session management**: creating, attaching, and driving onboard sessions
61/// between endpoint (source) and controller (destination) roles.
62/// - **Remote connectivity**: exchanging serialized layout metadata with peer
63/// instances so workers can perform RDMA transfers.
64/// - **Velo RPC**: registering handlers via `VeloLeaderService` so remote
65/// leaders can initiate sessions and exchange metadata.
66#[derive(Clone)]
67pub struct InstanceLeader {
68 /// Nova instance for distributed communication.
69 messenger: Arc<Messenger>,
70
71 /// Block registry for deduplication.
72 #[allow(dead_code)]
73 pub(crate) registry: BlockRegistry,
74
75 /// G2 (host memory) block manager (wrapped in Arc since BlockManager doesn't implement Clone).
76 pub(crate) g2_manager: Arc<BlockManager<G2>>,
77
78 /// Optional G3 (disk) block manager
79 pub(crate) g3_manager: Option<Arc<BlockManager<G3>>>,
80
81 /// Workers for executing transfers (at least 1 required).
82 /// Multiple workers enable parallel transfers and redundancy.
83 workers: Vec<Arc<dyn Worker>>,
84
85 /// Parallel worker abstraction wrapping the workers.
86 /// Used for RDMA transfers with proper handle mapping storage.
87 parallel_worker: Option<Arc<dyn ParallelWorkers>>,
88
89 /// Map of active sessions (session_id -> message channel).
90 sessions: Arc<DashMap<SessionId, OnboardSessionTx>>,
91
92 /// Cached worker metadata (avoids querying workers repeatedly).
93 cached_worker_metadata: Option<Vec<SerializedLayout>>,
94
95 /// Map of session states for holding blocks alive (RAII).
96 session_states: Arc<DashMap<SessionId, SessionState>>,
97
98 /// List of remote leader instance IDs (mutable for post-construction configuration).
99 remote_leaders: Arc<std::sync::RwLock<Vec<InstanceId>>>,
100
101 /// Message transport for session communication.
102 transport: Arc<MessageTransport>,
103
104 // ========================================================================
105 // Unified Session Protocol
106 // ========================================================================
107 /// Map of session message receivers.
108 /// Used by SessionHandle/SessionEndpoint/ControllableSession.
109 session_sessions: Arc<DashMap<SessionId, SessionMessageTx>>,
110
111 // ========================================================================
112 // G4/Object Storage
113 // ========================================================================
114 /// Object storage client for G4 search and load operations.
115 /// Leader calls has_blocks on S3 directly, coordinates workers for get_blocks.
116 object_client: Option<Arc<dyn ObjectBlockOps>>,
117}
118
119/// Builder for InstanceLeader.
120#[derive(Default)]
121pub struct InstanceLeaderBuilder {
122 messenger: Option<Arc<Messenger>>,
123 registry: Option<BlockRegistry>,
124 g2_manager: Option<Arc<BlockManager<G2>>>,
125 g3_manager: Option<Arc<BlockManager<G3>>>,
126 workers: Vec<Arc<dyn Worker>>,
127 sessions: Option<Arc<DashMap<SessionId, OnboardSessionTx>>>,
128 remote_leaders: Option<Vec<InstanceId>>,
129 cached_worker_metadata: Option<Vec<SerializedLayout>>,
130 object_client: Option<Arc<dyn ObjectBlockOps>>,
131}
132
133impl InstanceLeaderBuilder {
134 /// Initialize builder with components from KvbmRuntime.
135 ///
136 /// This extracts Nova from the runtime. Use this when the runtime
137 /// has already been constructed and you want the leader to share
138 /// the same Nova instance for distributed communication.
139 ///
140 /// # Example
141 /// ```ignore
142 /// let runtime = KvbmRuntime::from_env_leader().await?;
143 /// let leader = InstanceLeaderBuilder::default()
144 /// .with_runtime(&runtime)
145 /// .g2_manager(g2_manager)
146 /// .build()?;
147 /// ```
148 pub fn with_runtime(self, runtime: &crate::KvbmRuntime) -> Self {
149 self.messenger(runtime.messenger().clone())
150 }
151
152 pub fn messenger(mut self, messenger: Arc<Messenger>) -> Self {
153 self.messenger = Some(messenger);
154 self
155 }
156
157 pub fn registry(mut self, registry: BlockRegistry) -> Self {
158 self.registry = Some(registry);
159 self
160 }
161
162 pub fn with_g2_manager(mut self, manager: Option<BlockManager<G2>>) -> Self {
163 self.g2_manager = manager.map(Arc::new);
164 self
165 }
166
167 pub fn with_g3_manager(mut self, manager: Option<BlockManager<G3>>) -> Self {
168 self.g3_manager = manager.map(Arc::new);
169 self
170 }
171
172 pub fn g2_manager(mut self, manager: Arc<BlockManager<G2>>) -> Self {
173 self.g2_manager = Some(manager);
174 self
175 }
176
177 pub fn g3_manager(mut self, manager: Arc<BlockManager<G3>>) -> Self {
178 self.g3_manager = Some(manager);
179 self
180 }
181
182 /// Add a single worker (convenience method).
183 pub fn worker(mut self, worker: Arc<dyn Worker>) -> Self {
184 self.workers.push(worker);
185 self
186 }
187
188 /// Set all workers at once.
189 pub fn workers(mut self, workers: Vec<Arc<dyn Worker>>) -> Self {
190 self.workers = workers;
191 self
192 }
193
194 pub fn remote_leaders(mut self, leaders: Vec<InstanceId>) -> Self {
195 self.remote_leaders = Some(leaders);
196 self
197 }
198
199 /// Cache worker metadata upfront to avoid querying workers later.
200 ///
201 /// This is useful when workers have already exported metadata during initialization
202 /// (e.g., in the connector pattern where workers return metadata in their init response).
203 pub fn with_cached_worker_metadata(mut self, metadata: Vec<SerializedLayout>) -> Self {
204 self.cached_worker_metadata = Some(metadata);
205 self
206 }
207
208 /// Set the object storage client for G4 search and load operations.
209 ///
210 /// The leader uses this client to:
211 /// - Query S3 for block presence via `has_blocks`
212 /// - Coordinate workers to load blocks from S3 via `get_blocks`
213 pub fn object_client(mut self, client: Arc<dyn ObjectBlockOps>) -> Self {
214 self.object_client = Some(client);
215 self
216 }
217
218 pub fn build(self) -> Result<InstanceLeader> {
219 let messenger = self
220 .messenger
221 .ok_or_else(|| anyhow::anyhow!("Nova instance required"))?;
222 let transport = Arc::new(MessageTransport::velo(messenger.clone()));
223
224 // Create event system for notification aggregation
225 let events = Arc::new(messenger.event_manager());
226
227 // Get current tokio runtime handle
228 let runtime = tokio::runtime::Handle::current();
229
230 // // Validate at least one worker
231 // if self.workers.is_empty() {
232 // anyhow::bail!("At least one worker required");
233 // }
234
235 // todo: we will need a common builder pattern for creating "general" parallel workers
236 // - we could also use an enum and match as the number of types will be limited
237
238 // Create parallel worker if workers are provided
239 let parallel_worker: Option<Arc<dyn ParallelWorkers>> = if !self.workers.is_empty() {
240 Some(Arc::new(SpmdParallelWorkers::new(
241 self.workers.to_vec(),
242 events.clone(),
243 runtime.clone(),
244 )))
245 } else {
246 None
247 };
248
249 Ok(InstanceLeader {
250 messenger,
251 registry: self
252 .registry
253 .ok_or_else(|| anyhow::anyhow!("block registry required"))?,
254 g2_manager: self
255 .g2_manager
256 .ok_or_else(|| anyhow::anyhow!("g2_manager required"))?,
257 g3_manager: self.g3_manager,
258 workers: self.workers,
259 parallel_worker,
260 cached_worker_metadata: self.cached_worker_metadata,
261 sessions: self.sessions.unwrap_or_else(|| Arc::new(DashMap::new())),
262 session_states: Arc::new(DashMap::new()),
263 remote_leaders: Arc::new(std::sync::RwLock::new(
264 self.remote_leaders.unwrap_or_default(),
265 )),
266 transport,
267 session_sessions: Arc::new(DashMap::new()),
268 object_client: self.object_client,
269 })
270 }
271}
272
273/// Internal session state for holding matched blocks.
274#[allow(dead_code)] // Used for RAII block lifetime management
275struct SessionState {
276 session_id: SessionId,
277 matched_g2_blocks: Vec<ImmutableBlock<G2>>,
278 matched_g3_blocks: Vec<ImmutableBlock<G3>>,
279 status_tx: watch::Sender<OnboardingStatus>,
280}
281
282/// Result of scanning for blocks across tiers.
283///
284/// Unlike `FindMatchesResult`, this scans all given hashes without stopping on first miss.
285/// Returns blocks found in each tier along with their sorted positions.
286pub struct ScanBlocksResult {
287 /// Blocks found in G2 (host memory).
288 pub g2_blocks: HashMap<SequenceHash, ImmutableBlock<G2>>,
289
290 /// Blocks found in G3 (disk).
291 pub g3_blocks: HashMap<SequenceHash, ImmutableBlock<G3>>,
292
293 /// All found blocks sorted by position (lowest to highest).
294 /// Each entry indicates which tier (G2/G3) the block was found in.
295 pub sorted_matches: Vec<(SequenceHash, LogicalLayoutHandle)>,
296}
297
298impl InstanceLeader {
299 /// Get a reference to the G2 BlockManager.
300 pub fn g2_manager(&self) -> &Arc<BlockManager<G2>> {
301 &self.g2_manager
302 }
303
304 /// Get a reference to the optional G3 BlockManager.
305 pub fn g3_manager(&self) -> Option<&Arc<BlockManager<G3>>> {
306 self.g3_manager.as_ref()
307 }
308
309 /// Get the block registry.
310 pub fn registry(&self) -> &BlockRegistry {
311 &self.registry
312 }
313
314 /// Get a reference to the Nova instance.
315 ///
316 /// This provides access to the Nova distributed system for features
317 /// like event coordination and cross-instance communication.
318 pub fn messenger(&self) -> &Arc<Messenger> {
319 &self.messenger
320 }
321
322 /// Get the tokio runtime handle from Nova.
323 ///
324 /// This handle should be used for spawning background tasks that need to
325 /// run on the KVBM runtime's executor (e.g., offload engine pipelines).
326 pub fn runtime(&self) -> tokio::runtime::Handle {
327 self.messenger.runtime().clone()
328 }
329
330 /// Check if a parallel_worker is configured.
331 ///
332 /// The parallel_worker is required for local transfer operations
333 /// (e.g., offloading blocks between tiers).
334 pub fn has_parallel_worker(&self) -> bool {
335 self.parallel_worker.is_some()
336 }
337
338 /// Get the parallel worker for distributed operations.
339 ///
340 /// The parallel worker fans out operations to all workers and aggregates results.
341 /// It implements `ObjectBlockOps` for coordinated object storage uploads.
342 pub fn parallel_worker(&self) -> Option<Arc<dyn ParallelWorkers>> {
343 self.parallel_worker.clone()
344 }
345
346 /// Get the object storage client for G4 operations.
347 ///
348 /// Returns `Some` if object storage is configured, `None` otherwise.
349 /// The client is used by InitiatorSession for G4 parallel search.
350 pub fn object_client(&self) -> Option<Arc<dyn ObjectBlockOps>> {
351 self.object_client.clone()
352 }
353
354 /// Add a remote leader to the search list.
355 ///
356 /// Remote leaders are queried during `find_matches_with_options` when
357 /// `search_remote == true`. This method allows adding remote leaders
358 /// after construction (e.g., when instance IDs are only known after
359 /// cluster setup).
360 pub fn add_remote_leader(&self, instance_id: InstanceId) {
361 let mut remote_leaders = self.remote_leaders.write().unwrap();
362 if !remote_leaders.contains(&instance_id) {
363 remote_leaders.push(instance_id);
364 }
365 }
366
367 /// Set all remote leaders at once.
368 pub fn set_remote_leaders(&self, instance_ids: Vec<InstanceId>) {
369 let mut remote_leaders = self.remote_leaders.write().unwrap();
370 *remote_leaders = instance_ids;
371 }
372
373 /// Get the list of remote leader instance IDs.
374 pub fn remote_leaders(&self) -> Vec<InstanceId> {
375 self.remote_leaders.read().unwrap().clone()
376 }
377
378 /// Scan for all blocks matching any of the given sequence hashes.
379 ///
380 /// Unlike `find_matches`, this:
381 /// - Does NOT stop on first miss
382 /// - Returns blocks from both G2 and G3 tiers separately
383 /// - Acquires blocks from pools (caller owns until dropped via RAII)
384 /// - Returns `sorted_matches` ordered by `SequenceHash::position()`
385 ///
386 /// # Arguments
387 /// * `sequence_hashes` - Hashes to scan for
388 /// * `touch` - Whether to update frequency tracking (for MultiLRU eviction policy)
389 ///
390 /// # Algorithm
391 /// 1. Scan G2 manager for candidates
392 /// 2. Scan G3 manager for remaining candidates
393 /// 3. Build sorted_matches from both, sorted by position (lowest to highest)
394 pub fn scan_blocks(&self, sequence_hashes: &[SequenceHash], touch: bool) -> ScanBlocksResult {
395 // Step 1: Scan G2 for all candidates
396 let g2_blocks = self.g2_manager.scan_matches(sequence_hashes, touch);
397
398 // Step 2: Find remaining hashes not in G2
399 let remaining: Vec<SequenceHash> = sequence_hashes
400 .iter()
401 .filter(|h| !g2_blocks.contains_key(h))
402 .copied()
403 .collect();
404
405 // Step 3: Scan G3 for remaining (if G3 exists)
406 let g3_blocks = if let Some(ref g3_manager) = self.g3_manager {
407 if !remaining.is_empty() {
408 g3_manager.scan_matches(&remaining, touch)
409 } else {
410 HashMap::new()
411 }
412 } else {
413 HashMap::new()
414 };
415
416 // Step 4: Build sorted_matches from both tiers
417 let mut sorted_matches: Vec<(SequenceHash, LogicalLayoutHandle)> =
418 Vec::with_capacity(g2_blocks.len() + g3_blocks.len());
419
420 // Add G2 matches
421 for hash in g2_blocks.keys() {
422 sorted_matches.push((*hash, LogicalLayoutHandle::G2));
423 }
424
425 // Add G3 matches
426 for hash in g3_blocks.keys() {
427 sorted_matches.push((*hash, LogicalLayoutHandle::G3));
428 }
429
430 // Sort by SequenceHash position (lowest to highest)
431 sorted_matches.sort_by_key(|(hash, _)| hash.position());
432
433 ScanBlocksResult {
434 g2_blocks,
435 g3_blocks,
436 sorted_matches,
437 }
438 }
439
440 /// Scan blocks using a custom policy that controls iteration and yields results.
441 ///
442 /// This provides maximum flexibility for implementing custom scanning strategies.
443 /// The policy receives access to a `BlockAccessor` for acquiring blocks and a
444 /// `PolicyContext` for yielding results incrementally.
445 ///
446 /// # Arguments
447 /// * `hashes` - Sequence hashes to scan
448 /// * `touch` - Whether to update frequency tracking on block access
449 /// * `policy` - Function that implements the scanning strategy
450 ///
451 /// # Design
452 ///
453 /// The accessor does NOT hold locks between calls. Each `.find()` call is
454 /// independent. This enables:
455 /// - Custom iteration patterns (sorted, BTree scan, binary search, etc.)
456 /// - Yielding results incrementally (e.g., contiguous subsequences)
457 /// - Future parallel execution (accessor is Send + Sync)
458 ///
459 /// # Example: Simple linear scan
460 /// ```ignore
461 /// let blocks = leader.scan_with_policy(&hashes, true, |hashes, ctx| {
462 /// for hash in hashes {
463 /// if let Some(block) = ctx.accessor().find(*hash) {
464 /// ctx.yield_item(block);
465 /// }
466 /// }
467 /// });
468 /// ```
469 ///
470 /// # Example: Find contiguous subsequences
471 /// ```ignore
472 /// let runs: Vec<Vec<TieredBlock>> = leader.scan_with_policy(&hashes, true, |hashes, ctx| {
473 /// let mut run = Vec::new();
474 /// let mut last_pos: Option<u64> = None;
475 ///
476 /// for hash in hashes.iter().sorted_by_key(|h| h.position()) {
477 /// if let Some(block) = ctx.accessor().find(*hash) {
478 /// let pos = block.position();
479 /// if last_pos.map_or(true, |p| pos == p + 1) {
480 /// run.push(block);
481 /// } else {
482 /// if !run.is_empty() { ctx.yield_item(std::mem::take(&mut run)); }
483 /// run.push(block);
484 /// }
485 /// last_pos = Some(pos);
486 /// } else if !run.is_empty() {
487 /// ctx.yield_item(std::mem::take(&mut run));
488 /// last_pos = None;
489 /// }
490 /// }
491 /// if !run.is_empty() { ctx.yield_item(run); }
492 /// });
493 /// ```
494 pub fn scan_with_policy<F, T>(&self, hashes: &[SequenceHash], touch: bool, policy: F) -> Vec<T>
495 where
496 F: FnOnce(&[SequenceHash], &mut PolicyContext<T>),
497 {
498 let accessor = BlockAccessor::new(self, touch);
499 let mut ctx = PolicyContext {
500 accessor,
501 results: Vec::new(),
502 };
503 policy(hashes, &mut ctx);
504 ctx.results
505 }
506
507 pub fn builder() -> InstanceLeaderBuilder {
508 InstanceLeaderBuilder::default()
509 }
510
511 /// Register Nova handlers for leader-to-leader communication.
512 ///
513 /// This must be called after construction to enable distributed onboarding.
514 pub fn register_handlers(&self) -> Result<()> {
515 let instance_id = self.messenger.instance_id();
516 let g2_manager = self.g2_manager.clone();
517 let g3_manager = self.g3_manager.clone();
518 let parallel_worker = self.parallel_worker.clone();
519 let transport = self.transport.clone();
520 let sessions = self.sessions.clone();
521
522 let spawn_responder = move |msg: OnboardMessage| -> Result<()> {
523 if let OnboardMessage::CreateSession {
524 requester,
525 session_id,
526 sequence_hashes,
527 } = msg
528 {
529 let (tx, rx) = mpsc::channel(100);
530 sessions.insert(session_id, tx);
531
532 let session = ResponderSession::new(
533 session_id,
534 instance_id,
535 requester,
536 g2_manager.clone(),
537 g3_manager.clone(),
538 parallel_worker.clone(),
539 transport.clone(),
540 );
541
542 tokio::spawn(async move {
543 if let Err(e) = session.run(rx, sequence_hashes).await {
544 tracing::warn!(error = %e, "ResponderSession error");
545 }
546 });
547
548 Ok(())
549 } else {
550 anyhow::bail!("spawn_responder called with non-CreateSession message")
551 }
552 };
553
554 // Create export_metadata callback if we have workers or cached metadata
555 let export_metadata_callback: Option<ExportMetadataCallback> =
556 if !self.workers.is_empty() || self.cached_worker_metadata.is_some() {
557 let workers = self.workers.clone();
558 let cached_metadata = self.cached_worker_metadata.clone();
559 Some(Arc::new(move || {
560 let workers = workers.clone();
561 let cached_metadata = cached_metadata.clone();
562 Box::pin(async move {
563 // Return cached metadata if available
564 if let Some(cached) = cached_metadata {
565 return Ok(cached);
566 }
567 // Otherwise, query workers
568 let mut metadata = Vec::with_capacity(workers.len());
569 for worker in &workers {
570 let serialized = worker.export_metadata()?.await?;
571 metadata.push(serialized);
572 }
573 Ok(metadata)
574 })
575 }))
576 } else {
577 None
578 };
579
580 let mut service = VeloLeaderService::new(self.messenger.clone(), self.sessions.clone())
581 .with_spawn_responder(spawn_responder)
582 .with_session_sessions(self.session_sessions.clone());
583
584 if let Some(callback) = export_metadata_callback {
585 service = service.with_export_metadata(callback);
586 }
587
588 service.register_handlers()?;
589
590 Ok(())
591 }
592
593 /// Store session state (held blocks and status channel).
594 ///
595 /// Blocks are kept alive via RAII until the session is removed from storage.
596 fn store_session_state(&self, state: SessionState) {
597 self.session_states.insert(state.session_id, state);
598 }
599
600 /// Release a completed session, dropping any held blocks.
601 ///
602 /// This is optional - sessions will naturally be cleaned up when the InstanceLeader
603 /// is dropped. Call this explicitly if you need to release blocks earlier.
604 pub fn release_session(&self, session_id: SessionId) {
605 self.session_states.remove(&session_id);
606 self.sessions.remove(&session_id);
607 self.session_sessions.remove(&session_id);
608 }
609
610 // ========================================================================
611 // Inverted Control Pattern (Prefill-Decode) Methods
612 // ========================================================================
613
614 /// Create a controllable session for local blocks.
615 ///
616 /// This is the "Decode side" of the inverted control pattern:
617 /// 1. Search local G2 and G3 for matches
618 /// 2. Create a ControllableSession that holds the blocks
619 /// 3. Return session_id to be sent to Prefill out-of-band
620 ///
621 /// By default, G3→G2 staging starts immediately (auto_stage=true).
622 pub fn create_controllable_session(
623 &self,
624 sequence_hashes: &[SequenceHash],
625 ) -> Result<ControllableSessionResult> {
626 self.create_controllable_session_with_options(
627 sequence_hashes,
628 ControllableSessionOptions::default(),
629 )
630 }
631
632 /// Create a controllable session with custom options.
633 ///
634 /// Use this when you need to control auto-staging behavior.
635 pub fn create_controllable_session_with_options(
636 &self,
637 sequence_hashes: &[SequenceHash],
638 options: ControllableSessionOptions,
639 ) -> Result<ControllableSessionResult> {
640 let session_id = SessionId::from(Uuid::new_v4());
641
642 // Local search only
643 let matched_g2_blocks = self.g2_manager.match_blocks(sequence_hashes);
644
645 // Find remaining hashes not in G2
646 let remaining_hashes: Vec<_> = sequence_hashes
647 .iter()
648 .filter(|h| !matched_g2_blocks.iter().any(|b| b.sequence_hash() == **h))
649 .copied()
650 .collect();
651
652 // Search G3 for remaining hashes
653 let matched_g3_blocks = if let Some(ref g3_manager) = self.g3_manager {
654 g3_manager.match_blocks(&remaining_hashes)
655 } else {
656 Vec::new()
657 };
658
659 let local_g2_count = matched_g2_blocks.len();
660 let local_g3_count = matched_g3_blocks.len();
661
662 // Create session channel using unified SessionMessage protocol
663 let (tx, rx) = session_message_channel(100);
664 self.session_sessions.insert(session_id, tx);
665
666 // Collect G2 layout handles from workers for round-robin block allocation
667 let worker_g2_handles: Vec<LayoutHandle> = self
668 .parallel_worker
669 .as_ref()
670 .map(|pw| pw.workers().iter().filter_map(|w| w.g2_handle()).collect())
671 .unwrap_or_default();
672
673 let endpoint = super::session::SessionEndpoint::new(
674 session_id,
675 self.messenger.instance_id(),
676 self.transport.clone(),
677 rx,
678 );
679
680 let (cmd_tx, cmd_rx) = mpsc::channel(16);
681
682 let session = ServerSession::new_with_staging(
683 endpoint,
684 BlockHolder::new(matched_g2_blocks),
685 BlockHolder::new(matched_g3_blocks),
686 worker_g2_handles,
687 self.g2_manager.clone(),
688 self.parallel_worker.clone(),
689 cmd_rx,
690 ServerSessionOptions {
691 auto_stage: options.auto_stage,
692 },
693 );
694
695 // Keep handle alive to prevent cmd channel from closing
696 let _handle = ServerSessionHandle::new(session_id, self.messenger.instance_id(), cmd_tx);
697
698 // Spawn session task
699 let session_sessions = self.session_sessions.clone();
700 tokio::spawn(async move {
701 let _handle = _handle; // move handle into task to keep cmd channel open
702 if let Err(e) = session.run().await {
703 tracing::warn!(error = %e, "ServerSession error");
704 }
705 // Clean up when session completes
706 session_sessions.remove(&session_id);
707 });
708
709 Ok(ControllableSessionResult {
710 session_id,
711 local_g2_count,
712 local_g3_count,
713 })
714 }
715
716 // ========================================================================
717 // Unified Session Protocol
718 // ========================================================================
719
720 /// Attach to a remote session.
721 /// Returns a `SessionHandle` that uses `SessionMessage` for communication.
722 ///
723 /// # Arguments
724 /// * `remote_instance` - The instance hosting the session
725 /// * `session_id` - The session to attach to
726 ///
727 /// # Example
728 /// ```ignore
729 /// let handle = leader.attach_session(remote_id, session_id).await?;
730 /// let state = handle.wait_for_ready().await?;
731 /// handle.trigger_staging().await?;
732 /// ```
733 pub async fn attach_session(
734 &self,
735 remote_instance: InstanceId,
736 session_id: SessionId,
737 ) -> Result<SessionHandle> {
738 // Create local channel for receiving state updates
739 let (state_tx, state_rx) = session_handle_state_channel();
740
741 // Register handler for this session's messages
742 let (msg_tx, msg_rx) = session_message_channel(100);
743 self.session_sessions.insert(session_id, msg_tx);
744
745 // Spawn receiver task to update state
746 tokio::spawn(Self::run_session_receiver(msg_rx, state_tx));
747
748 // Send attach message using new protocol
749 let msg = SessionMessage::Attach {
750 peer: self.messenger.instance_id(),
751 session_id,
752 as_role: ControlRole::Controller,
753 };
754 self.transport.send_session(remote_instance, msg).await?;
755
756 let mut handle = SessionHandle::new(
757 session_id,
758 remote_instance,
759 self.messenger.instance_id(),
760 self.transport.clone(),
761 state_rx,
762 );
763
764 // Add RDMA support if parallel worker is configured
765 if let Some(parallel_worker) = &self.parallel_worker {
766 handle = handle.with_rdma_support(parallel_worker.clone());
767 }
768
769 Ok(handle)
770 }
771
772 // ========================================================================
773 // Endpoint Session Creation (Server-Side)
774 // ========================================================================
775
776 /// Create an endpoint session that a remote peer can attach to.
777 ///
778 /// This searches local G2/G3 for blocks matching the given sequence hashes
779 /// and creates a session that exposes them for remote RDMA pull.
780 ///
781 /// Returns `(session_id, handle)` where:
782 /// - `session_id` - Send to remote peer for attachment
783 /// - `handle` - Use to control the session (send layer notifications, close)
784 ///
785 /// # Example
786 /// ```ignore
787 /// // Create session for sequence hashes
788 /// let (session_id, handle) = leader.create_endpoint_session(&hashes)?;
789 ///
790 /// // Send session_id to remote peer out-of-band
791 /// // Remote attaches via: remote_leader.attach_session(local_id, session_id)
792 ///
793 /// // For layerwise transfer, notify when layers are ready
794 /// handle.notify_layers_ready(0..1).await?;
795 /// ```
796 pub fn create_endpoint_session(
797 &self,
798 sequence_hashes: &[SequenceHash],
799 ) -> Result<(SessionId, ServerSessionHandle)> {
800 let session_id = SessionId::from(uuid::Uuid::new_v4());
801
802 // Local search
803 let matched_g2_blocks = self.g2_manager.match_blocks(sequence_hashes);
804
805 // Collect layout handles from workers
806 // Note: For single-worker setups, all blocks use the same handle
807 // For multi-worker (SPMD), each block gets the handle from its assigned worker
808 let worker_g2_handles: Vec<LayoutHandle> = self
809 .parallel_worker
810 .as_ref()
811 .map(|pw| pw.workers().iter().filter_map(|w| w.g2_handle()).collect())
812 .unwrap_or_default();
813
814 // Assign layout handle to each matched block
815 // For now, use the first worker's handle for all blocks (single-worker assumption)
816 // TODO: For SPMD, map blocks to worker handles based on block assignment
817 let layout_handle = worker_g2_handles
818 .first()
819 .copied()
820 .ok_or_else(|| anyhow::anyhow!("No G2 layout handle available from workers"))?;
821 let layout_handles: Vec<LayoutHandle> = vec![layout_handle; matched_g2_blocks.len()];
822
823 // Get sequence hashes from matched blocks
824 let matched_hashes: Vec<SequenceHash> = matched_g2_blocks
825 .iter()
826 .map(|b| b.sequence_hash())
827 .collect();
828
829 // Create the session channel
830 let (msg_tx, msg_rx) = session_message_channel(100);
831 self.session_sessions.insert(session_id, msg_tx);
832
833 // Create BlockHolder from matched blocks
834 let block_holder = BlockHolder::new(matched_g2_blocks);
835
836 // Create the session and handle
837 let (session, handle) = create_server_session(
838 session_id,
839 self.messenger.instance_id(),
840 block_holder,
841 layout_handles,
842 matched_hashes,
843 self.transport.clone(),
844 msg_rx,
845 );
846
847 // Spawn the session task
848 let session_sessions = self.session_sessions.clone();
849 tokio::spawn(async move {
850 if let Err(e) = session.run().await {
851 tracing::warn!(error = %e, "ServerSession error");
852 }
853 // Clean up when session completes
854 session_sessions.remove(&session_id);
855 });
856
857 Ok((session_id, handle))
858 }
859
860 /// Create an endpoint session for specific pre-allocated blocks.
861 ///
862 /// Unlike `create_endpoint_session`, this doesn't search - it uses the
863 /// provided blocks directly. Useful when the caller already has blocks
864 /// to expose (e.g., after prefill computation).
865 ///
866 /// # Arguments
867 /// * `blocks` - Blocks to expose for RDMA pull
868 /// * `sequence_hashes` - Sequence hashes for the blocks (must match block count)
869 /// * `layout_handles` - Layout handles for the blocks (must match block count)
870 ///
871 /// # Example
872 /// ```ignore
873 /// // After prefill computation, expose blocks for Decode to pull
874 /// let (session_id, handle) = leader.create_endpoint_session_for_blocks(
875 /// prefill_blocks,
876 /// &hashes,
877 /// &layout_handles,
878 /// )?;
879 /// ```
880 pub fn create_endpoint_session_for_blocks(
881 &self,
882 blocks: BlockHolder<G2>,
883 sequence_hashes: &[SequenceHash],
884 layout_handles: &[LayoutHandle],
885 ) -> Result<(SessionId, ServerSessionHandle)> {
886 let session_id = SessionId::from(uuid::Uuid::new_v4());
887
888 // Create the session channel
889 let (msg_tx, msg_rx) = session_message_channel(100);
890 self.session_sessions.insert(session_id, msg_tx);
891
892 // Create the session and handle
893 let (session, handle) = create_server_session(
894 session_id,
895 self.messenger.instance_id(),
896 blocks,
897 layout_handles.to_vec(),
898 sequence_hashes.to_vec(),
899 self.transport.clone(),
900 msg_rx,
901 );
902
903 // Spawn the session task
904 let session_sessions = self.session_sessions.clone();
905 tokio::spawn(async move {
906 if let Err(e) = session.run().await {
907 tracing::warn!(error = %e, "ServerSession error");
908 }
909 // Clean up when session completes
910 session_sessions.remove(&session_id);
911 });
912
913 Ok((session_id, handle))
914 }
915
916 /// Internal: Process incoming SessionMessage for a session.
917 async fn run_session_receiver(
918 mut rx: mpsc::Receiver<SessionMessage>,
919 state_tx: super::session::SessionHandleStateTx,
920 ) {
921 while let Some(msg) = rx.recv().await {
922 match msg {
923 SessionMessage::StateResponse { state, .. } => {
924 state_tx.update(state);
925 }
926 SessionMessage::BlocksStaged {
927 staged_blocks,
928 remaining,
929 layer_range,
930 ..
931 } => {
932 state_tx.add_staged_blocks(staged_blocks, remaining, layer_range);
933 }
934 SessionMessage::Error { message, .. } => {
935 tracing::warn!(%message, "Session error");
936 state_tx.set_failed();
937 break;
938 }
939 SessionMessage::Close { .. } => {
940 state_tx.set_phase(SessionPhase::Complete);
941 break;
942 }
943 _ => {
944 // Ignore control commands (sent by controller, not received)
945 }
946 }
947 }
948 }
949
950 /// Get the session sessions map (for Nova handler registration).
951 #[expect(dead_code)]
952 pub(crate) fn session_sessions(&self) -> Arc<DashMap<SessionId, SessionMessageTx>> {
953 self.session_sessions.clone()
954 }
955
956 // ========================================================================
957 // RDMA Metadata Management
958 // These methods handle layout metadata export/import for remote RDMA transfers.
959 // ========================================================================
960
961 /// Check if metadata for a remote instance has been loaded.
962 ///
963 /// Returns true if `import_remote_metadata` has been successfully called
964 /// for the given instance.
965 pub fn has_remote_metadata(&self, instance: InstanceId) -> bool {
966 self.parallel_worker
967 .as_ref()
968 .map(|pw| pw.has_remote_metadata(instance))
969 .unwrap_or(false)
970 }
971
972 /// Get the number of workers attached to this leader.
973 pub fn worker_count(&self) -> usize {
974 self.workers.len()
975 }
976
977 /// Export metadata from all workers.
978 ///
979 /// Returns a `Vec<SerializedLayout>` where each element corresponds to a worker
980 /// in rank order. This metadata can be sent to remote instances to enable
981 /// RDMA transfers.
982 ///
983 /// # Returns
984 /// Vector of serialized layouts, one per worker
985 pub async fn export_worker_metadata(&self) -> Result<Vec<SerializedLayout>> {
986 // Return cached metadata if available
987 if let Some(cached) = &self.cached_worker_metadata {
988 return Ok(cached.clone());
989 }
990
991 // Otherwise, query workers
992 let mut metadata = Vec::with_capacity(self.workers.len());
993
994 for worker in &self.workers {
995 let serialized = worker.export_metadata()?.await?;
996 metadata.push(serialized);
997 }
998
999 Ok(metadata)
1000 }
1001
1002 /// Import metadata from a remote instance's workers.
1003 ///
1004 /// This imports layout metadata from a remote instance, enabling RDMA transfers
1005 /// to pull data from that instance. Metadata is imported rank-by-rank:
1006 /// - local worker 0 imports remote worker 0's metadata
1007 /// - local worker 1 imports remote worker 1's metadata
1008 /// - etc.
1009 ///
1010 /// # Arguments
1011 /// * `remote_instance` - The instance ID of the remote leader
1012 /// * `metadata` - Vector of SerializedLayout from remote workers (one per worker)
1013 ///
1014 /// # Errors
1015 /// Returns an error if:
1016 /// - No parallel worker configured
1017 /// - Metadata was already imported for this instance
1018 /// - Worker count mismatch between local and remote
1019 /// - Individual worker metadata import fails
1020 pub async fn import_remote_metadata(
1021 &self,
1022 remote_instance: InstanceId,
1023 metadata: Vec<SerializedLayout>,
1024 ) -> Result<()> {
1025 let parallel_worker = self
1026 .parallel_worker
1027 .as_ref()
1028 .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1029
1030 // Check if already loaded
1031 if parallel_worker.has_remote_metadata(remote_instance) {
1032 anyhow::bail!("Metadata already imported for instance {}", remote_instance);
1033 }
1034
1035 // Connect to remote - this imports metadata and stores handle mappings
1036 parallel_worker
1037 .connect_remote(remote_instance, metadata)?
1038 .await?;
1039
1040 Ok(())
1041 }
1042
1043 // ========================================================================
1044 // Private Worker Mirror Methods
1045 // These methods execute operations across all workers and aggregate results.
1046 // ========================================================================
1047
1048 /// Execute local transfer across all workers, returning aggregated notification.
1049 ///
1050 /// Delegates to the parallel_worker which fans out to all workers and
1051 /// aggregates their notifications into a single composite notification.
1052 #[allow(dead_code)]
1053 pub(crate) fn execute_local_transfer(
1054 &self,
1055 src: LogicalLayoutHandle,
1056 dst: LogicalLayoutHandle,
1057 src_block_ids: Vec<BlockId>,
1058 dst_block_ids: Vec<BlockId>,
1059 options: TransferOptions,
1060 ) -> Result<TransferCompleteNotification> {
1061 let parallel_worker = self
1062 .parallel_worker
1063 .as_ref()
1064 .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1065
1066 parallel_worker.execute_local_transfer(
1067 src,
1068 dst,
1069 Arc::from(src_block_ids),
1070 Arc::from(dst_block_ids),
1071 options,
1072 )
1073 }
1074
1075 /// Execute remote onboard across all workers, returning aggregated notification.
1076 ///
1077 /// Delegates to the parallel_worker which fans out to all workers and
1078 /// aggregates their notifications into a single composite notification.
1079 #[allow(dead_code)]
1080 pub(crate) fn execute_remote_onboard(
1081 &self,
1082 src: RemoteDescriptor,
1083 dst: LogicalLayoutHandle,
1084 dst_block_ids: Vec<BlockId>,
1085 options: TransferOptions,
1086 ) -> Result<TransferCompleteNotification> {
1087 let parallel_worker = self
1088 .parallel_worker
1089 .as_ref()
1090 .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1091
1092 parallel_worker.execute_remote_onboard(src, dst, Arc::from(dst_block_ids), options)
1093 }
1094
1095 /// Execute remote offload across all workers, returning aggregated notification.
1096 ///
1097 /// Delegates to the parallel_worker which fans out to all workers and
1098 /// aggregates their notifications into a single composite notification.
1099 #[allow(dead_code)]
1100 pub(crate) fn execute_remote_offload(
1101 &self,
1102 src: LogicalLayoutHandle,
1103 dst: RemoteDescriptor,
1104 src_block_ids: Vec<BlockId>,
1105 options: TransferOptions,
1106 ) -> Result<TransferCompleteNotification> {
1107 let parallel_worker = self
1108 .parallel_worker
1109 .as_ref()
1110 .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1111
1112 parallel_worker.execute_remote_offload(src, Arc::from(src_block_ids), dst, options)
1113 }
1114}
1115
1116impl Leader for InstanceLeader {
1117 fn find_matches_with_options(
1118 &self,
1119 sequence_hashes: &[SequenceHash],
1120 options: FindMatchesOptions,
1121 ) -> Result<FindMatchesResult> {
1122 // Search G2 (host memory) for matches
1123 // Uses match_blocks which stops at first miss (implements "first hole" policy).
1124 // This ensures we only find contiguous blocks from the start of the sequence.
1125 // For distributed search, remote instances use scan_matches for broad coverage,
1126 // then first-hole filtering is applied in InitiatorSession after aggregation.
1127
1128 // todo: add explicit timing tracing here
1129 // let start_time = Instant::now();
1130 let matched_g2_blocks = self.g2_manager.match_blocks(sequence_hashes);
1131 //let g2_search_time = Instant::now().duration_since(start_time);
1132
1133 // Search G3 (disk) for remaining hashes if G3 is available
1134 let remaining_hashes: Vec<_> = sequence_hashes
1135 .iter()
1136 .filter(|h| !matched_g2_blocks.iter().any(|b| b.sequence_hash() == **h))
1137 .copied()
1138 .collect();
1139
1140 let matched_g3_blocks = if let Some(ref g3_manager) = self.g3_manager {
1141 // Uses match_blocks on remaining hashes (those not found in G2).
1142 // Since G2 already applied first-hole policy, G3 search continues from where G2 stopped.
1143 g3_manager.match_blocks(&remaining_hashes)
1144 } else {
1145 Vec::new()
1146 };
1147
1148 // Determine if we can return immediately (Ready) or need async session
1149 // Ready if:
1150 // - g3 blocks is empty
1151 // - AND NOT (search_remote AND has_remote_leaders)
1152 // - AND NOT (search_remote AND has_object_client)
1153 //
1154 // AsyncSession (is_ready=false) if:
1155 // - g3 is not empty, or
1156 // - search_remote is true AND (has_remote_leaders OR has_object_client)
1157 let has_remote_leaders = !self.remote_leaders.read().unwrap().is_empty();
1158 let has_object_client = self.object_client.is_some();
1159 let needs_remote_search =
1160 options.search_remote && (has_remote_leaders || has_object_client);
1161 let is_ready = matched_g3_blocks.is_empty() && !needs_remote_search;
1162
1163 if is_ready {
1164 // No session needed - blocks owned directly by ReadyResult (RAII)
1165 return Ok(FindMatchesResult::Ready(ReadyResult::new(
1166 matched_g2_blocks,
1167 )));
1168 }
1169
1170 // AsyncSession path: G3 blocks found or remote search enabled
1171 let session_id = SessionId::from(Uuid::new_v4());
1172 let local_g2_count = matched_g2_blocks.len();
1173 let local_g3_count = matched_g3_blocks.len();
1174
1175 // AsyncSession: staging locally and/or remote searching
1176 let (status_tx, status_rx) = watch::channel(OnboardingStatus::Searching);
1177 let all_g2_blocks = Arc::new(Mutex::new(None));
1178
1179 // Store session state to keep blocks alive
1180 let state = SessionState {
1181 session_id,
1182 matched_g2_blocks,
1183 matched_g3_blocks,
1184 status_tx: status_tx.clone(),
1185 };
1186 self.store_session_state(state);
1187
1188 // If no remote search, handle local-only staging
1189 if !options.search_remote {
1190 // Local-only staging (Prepare or Full mode)
1191 // TODO: Implement local G3→G2 staging
1192 let total_matched = local_g2_count + local_g3_count;
1193 status_tx
1194 .send(OnboardingStatus::Complete {
1195 matched_blocks: total_matched,
1196 })
1197 .ok();
1198
1199 return Ok(FindMatchesResult::AsyncSession(AsyncSessionResult::new(
1200 session_id,
1201 status_rx,
1202 all_g2_blocks,
1203 None, // No session handle for local-only staging (yet)
1204 )));
1205 }
1206
1207 // Remote search path
1208 let (tx, rx) = mpsc::channel(100);
1209 self.sessions.insert(session_id, tx);
1210
1211 // Create control channel for Hold/Prepare modes
1212 let (session_handle, control_rx) = if matches!(
1213 options.staging_mode,
1214 StagingMode::Hold | StagingMode::Prepare
1215 ) {
1216 let (control_tx, control_rx) = mpsc::channel(10);
1217 let handle = LegacySessionHandle::new(session_id, options.staging_mode, control_tx);
1218 (Some(handle), Some(control_rx))
1219 } else {
1220 (None, None)
1221 };
1222
1223 let session = InitiatorSession::new(
1224 session_id,
1225 self.messenger.instance_id(),
1226 options.staging_mode,
1227 self.g2_manager.clone(),
1228 self.g3_manager.clone(),
1229 self.parallel_worker.clone(),
1230 self.transport.clone(),
1231 status_tx.clone(),
1232 all_g2_blocks.clone(),
1233 control_rx.unwrap_or_else(|| {
1234 let (_, rx) = mpsc::channel(1);
1235 rx
1236 }),
1237 self.object_client.clone(),
1238 );
1239
1240 let remote_leaders = self.remote_leaders.read().unwrap().clone();
1241 let sequence_hashes = sequence_hashes.to_vec();
1242
1243 let handle = self.messenger.runtime();
1244
1245 handle.spawn(async move {
1246 if let Err(e) = session.run(rx, remote_leaders, sequence_hashes).await {
1247 tracing::warn!(error = %e, "InitiatorSession error");
1248 // Try to update status to indicate error
1249 status_tx
1250 .send(OnboardingStatus::Complete { matched_blocks: 0 })
1251 .ok();
1252 }
1253 });
1254
1255 Ok(FindMatchesResult::AsyncSession(AsyncSessionResult::new(
1256 session_id,
1257 status_rx,
1258 all_g2_blocks,
1259 session_handle,
1260 )))
1261 }
1262}