Skip to main content

kvbm_engine/worker/
coordinated.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! CoordinatedWorker - Leader's view of a worker with coordination state.
5//!
6//! This module provides a wrapper around the Worker trait that adds coordination
7//! state needed by the leader, including local layout handles and remote handle
8//! mappings for cross-leader transfers.
9
10use std::collections::HashMap;
11use std::sync::{OnceLock, RwLock};
12
13use anyhow::Result;
14use futures::future::BoxFuture;
15
16use crate::object::ObjectBlockOps;
17use crate::{BlockId, InstanceId, SequenceHash};
18use kvbm_physical::manager::LayoutHandle;
19use kvbm_physical::transfer::TransferOptions;
20
21use super::{
22    LogicalLayoutHandle, RemoteDescriptor, SerializedLayout, TransferCompleteNotification, Worker,
23    WorkerLayoutResponse,
24};
25
26/// Leader's view of a worker with coordination state.
27///
28/// # Coordination State vs Execution State
29///
30/// CoordinatedWorker maintains **coordination state** - the leader's view of what
31/// handles a worker has and how to route transfers. This is distinct from
32/// **execution state** which [`DirectWorker`] maintains for actual transfer execution.
33///
34/// | State Type | Owner | Purpose |
35/// |------------|-------|---------|
36/// | Execution | DirectWorker | Handles needed by TransferManager to execute |
37/// | Coordination | CoordinatedWorker | Leader's tracking for routing decisions |
38///
39/// When the inner worker is a DirectWorker, handles exist in both places. This
40/// duplication is intentional:
41/// - DirectWorker needs handles to call TransferManager
42/// - CoordinatedWorker provides uniform API for local AND remote workers
43/// - VeloWorkerClient is stateless, so leader must track handles somewhere
44///
45/// # Usage
46///
47/// ```ignore
48/// // Leader creates CoordinatedWorker wrapping actual worker
49/// let worker = CoordinatedWorker::new(
50///     Box::new(direct_worker),
51///     rank,
52///     host_instance,
53/// );
54///
55/// // After configure_layouts RPC, populate coordination state
56/// worker.apply_layout_response(&response)?;
57///
58/// // Leader can now query handles for routing
59/// if let Some(g2) = worker.local_g2() {
60///     // Route G2 transfers through this worker
61/// }
62/// ```
63///
64/// # Remote Handle Mappings
65///
66/// For cross-leader transfers (e.g., Prefill pulling from Decode), the leader
67/// imports remote worker metadata and stores rank-aware mappings:
68///
69/// ```ignore
70/// // Prefill leader imports Decode workers' metadata
71/// worker.import_remote_metadata(decode_leader_id, decode_rank, metadata).await?;
72///
73/// // Later, execute transfer using stored mapping
74/// worker.transfer_from_remote(
75///     decode_leader_id,
76///     decode_rank,
77///     LogicalLayoutHandle::G2,  // source
78///     src_block_ids,
79///     LogicalLayoutHandle::G2,  // destination
80///     dst_block_ids,
81///     options,
82/// )?;
83/// ```
84///
85/// [`DirectWorker`]: super::DirectWorker
86pub struct CoordinatedWorker {
87    /// The actual worker (local DirectWorker or remote VeloWorkerClient).
88    /// CoordinatedWorker delegates execution to this inner worker.
89    inner: Box<dyn Worker>,
90
91    /// This worker's rank under its leader (0-indexed).
92    /// Used for asymmetric TP routing between leaders with different worker counts.
93    rank: usize,
94
95    /// Instance ID of the process hosting this worker.
96    /// For DirectWorker: same as leader's instance.
97    /// For VeloWorkerClient: the remote worker's instance.
98    host_instance: InstanceId,
99
100    // =========================================================================
101    // Coordination State - leader's view of this worker's handles
102    // =========================================================================
103    /// G1 (GPU KV cache) layout handle.
104    /// Populated from WorkerLayoutResponse after configure_layouts RPC.
105    local_g1: OnceLock<LayoutHandle>,
106
107    /// G2 (Host/pinned cache) layout handle.
108    /// Populated from WorkerLayoutResponse after configure_layouts RPC.
109    local_g2: OnceLock<LayoutHandle>,
110
111    /// G3 (Disk cache) layout handle.
112    /// Populated from WorkerLayoutResponse after configure_layouts RPC.
113    local_g3: OnceLock<LayoutHandle>,
114
115    /// Remote handle mappings for cross-leader transfers.
116    /// Key: (remote_leader_id, remote_rank, logical_type) → physical_handle
117    ///
118    /// Unlike DirectWorker's remote_handles (keyed by instance only), this
119    /// includes rank for asymmetric TP routing. When Prefill (TP=4) pulls from
120    /// Decode (TP=2), each Prefill worker needs to know which Decode worker(s)
121    /// to pull from.
122    remote_handles: RwLock<HashMap<(InstanceId, usize, LogicalLayoutHandle), LayoutHandle>>,
123}
124
125impl CoordinatedWorker {
126    /// Create a new CoordinatedWorker wrapping an existing Worker.
127    pub fn new(inner: Box<dyn Worker>, rank: usize, host_instance: InstanceId) -> Self {
128        Self {
129            inner,
130            rank,
131            host_instance,
132            local_g1: OnceLock::new(),
133            local_g2: OnceLock::new(),
134            local_g3: OnceLock::new(),
135            remote_handles: RwLock::new(HashMap::new()),
136        }
137    }
138
139    /// Get this worker's rank.
140    pub fn rank(&self) -> usize {
141        self.rank
142    }
143
144    /// Get the instance ID of the process hosting this worker.
145    pub fn host_instance(&self) -> InstanceId {
146        self.host_instance
147    }
148
149    /// Get a reference to the underlying Worker.
150    pub fn inner(&self) -> &dyn Worker {
151        &*self.inner
152    }
153
154    /// Set the local G1 (GPU KV) handle.
155    ///
156    /// # Arguments
157    /// * `handle` - G1 layout handle
158    ///
159    /// # Errors
160    /// Returns error if G1 handle was already set.
161    pub fn set_local_g1(&self, handle: LayoutHandle) -> Result<()> {
162        self.local_g1
163            .set(handle)
164            .map_err(|_| anyhow::anyhow!("G1 handle already set"))
165    }
166
167    /// Set the local G2 (Host) handle.
168    ///
169    /// # Arguments
170    /// * `handle` - G2 layout handle
171    ///
172    /// # Errors
173    /// Returns error if G2 handle was already set.
174    pub fn set_local_g2(&self, handle: LayoutHandle) -> Result<()> {
175        self.local_g2
176            .set(handle)
177            .map_err(|_| anyhow::anyhow!("G2 handle already set"))
178    }
179
180    /// Set the local G3 (Disk) handle.
181    ///
182    /// # Arguments
183    /// * `handle` - G3 layout handle
184    ///
185    /// # Errors
186    /// Returns error if G3 handle was already set.
187    pub fn set_local_g3(&self, handle: LayoutHandle) -> Result<()> {
188        self.local_g3
189            .set(handle)
190            .map_err(|_| anyhow::anyhow!("G3 handle already set"))
191    }
192
193    /// Apply layout response from configure_layouts RPC.
194    ///
195    /// This is the primary way to populate coordination state. After the leader
196    /// sends a configure_layouts RPC to the worker, the response contains the
197    /// handles that were created. This method extracts those handles from the
198    /// serialized metadata.
199    ///
200    /// # Arguments
201    /// * `response` - The WorkerLayoutResponse from configure_layouts RPC
202    ///
203    /// # Example
204    /// ```ignore
205    /// // Leader calls configure_layouts on worker
206    /// let response = worker_client.configure_layouts(config).await?;
207    ///
208    /// // Populate coordination state from response
209    /// coordinated_worker.apply_layout_response(&response)?;
210    /// ```
211    pub fn apply_layout_response(&self, response: &WorkerLayoutResponse) -> Result<()> {
212        // Extract handles from the metadata
213        let unpacked = response.metadata.unpack()?;
214
215        for descriptor in &unpacked.layouts {
216            match descriptor.logical_type {
217                LogicalLayoutHandle::G1 => {
218                    let _ = self.local_g1.set(descriptor.handle);
219                }
220                LogicalLayoutHandle::G2 => {
221                    let _ = self.local_g2.set(descriptor.handle);
222                }
223                LogicalLayoutHandle::G3 => {
224                    let _ = self.local_g3.set(descriptor.handle);
225                }
226                LogicalLayoutHandle::G4 => {
227                    // G4 (object store) not tracked locally
228                }
229            }
230        }
231
232        Ok(())
233    }
234
235    /// Get the local G1 handle if set.
236    pub fn local_g1(&self) -> Option<LayoutHandle> {
237        self.local_g1.get().copied()
238    }
239
240    /// Get the local G2 handle if set.
241    pub fn local_g2(&self) -> Option<LayoutHandle> {
242        self.local_g2.get().copied()
243    }
244
245    /// Get the local G3 handle if set.
246    pub fn local_g3(&self) -> Option<LayoutHandle> {
247        self.local_g3.get().copied()
248    }
249
250    /// Import metadata from a remote worker and store handle mappings.
251    ///
252    /// This is called when the leader receives metadata from another leader's
253    /// workers during cross-leader coordination (e.g., prefill→decode).
254    ///
255    /// # Arguments
256    /// * `remote_leader_id` - Instance ID of the remote leader
257    /// * `remote_rank` - Rank of the remote worker under its leader
258    /// * `metadata` - Serialized layout metadata from the remote worker
259    pub async fn import_remote_metadata(
260        &self,
261        remote_leader_id: InstanceId,
262        remote_rank: usize,
263        metadata: SerializedLayout,
264    ) -> Result<()> {
265        // Unpack metadata to get logical type info
266        let unpacked = metadata.unpack()?;
267
268        // Import into the underlying worker so NIXL knows about the remote
269        let repacked = SerializedLayout::pack(
270            unpacked.worker_address.clone(),
271            unpacked.nixl_metadata.clone(),
272            unpacked.layouts.clone(),
273        )?;
274        let response = self.inner.import_metadata(repacked)?;
275        let _handles = response.await?;
276
277        // Store mappings for later lookups
278        let mut mapping = self.remote_handles.write().unwrap();
279        for descriptor in &unpacked.layouts {
280            mapping.insert(
281                (remote_leader_id, remote_rank, descriptor.logical_type),
282                descriptor.handle,
283            );
284        }
285
286        Ok(())
287    }
288
289    /// Look up physical handle for a remote transfer.
290    ///
291    /// # Arguments
292    /// * `remote_leader_id` - Instance ID of the remote leader
293    /// * `remote_rank` - Rank of the remote worker
294    /// * `logical_type` - Logical layout type (G1/G2/G3)
295    pub fn resolve_remote_handle(
296        &self,
297        remote_leader_id: InstanceId,
298        remote_rank: usize,
299        logical_type: LogicalLayoutHandle,
300    ) -> Option<LayoutHandle> {
301        self.remote_handles
302            .read()
303            .unwrap()
304            .get(&(remote_leader_id, remote_rank, logical_type))
305            .copied()
306    }
307
308    /// Check if remote metadata has been imported for a specific remote worker.
309    pub fn has_remote_metadata(&self, remote_leader_id: InstanceId, remote_rank: usize) -> bool {
310        let handles = self.remote_handles.read().unwrap();
311        handles
312            .keys()
313            .any(|(leader, rank, _)| *leader == remote_leader_id && *rank == remote_rank)
314    }
315
316    /// Execute transfer from a remote worker.
317    ///
318    /// This method looks up the remote handle from stored mappings and
319    /// executes an RDMA transfer to pull data from the remote worker.
320    ///
321    /// # Arguments
322    /// * `remote_leader_id` - Instance ID of the remote leader
323    /// * `remote_rank` - Rank of the source worker under its leader
324    /// * `src_logical` - Source logical layout type (e.g., G2)
325    /// * `src_block_ids` - Block IDs on the remote to pull
326    /// * `dst_logical` - Destination logical layout type on this worker
327    /// * `dst_block_ids` - Destination block IDs
328    /// * `options` - Transfer options
329    #[allow(clippy::too_many_arguments)]
330    pub fn transfer_from_remote(
331        &self,
332        remote_leader_id: InstanceId,
333        remote_rank: usize,
334        src_logical: LogicalLayoutHandle,
335        src_block_ids: Vec<BlockId>,
336        dst_logical: LogicalLayoutHandle,
337        dst_block_ids: std::sync::Arc<[BlockId]>,
338        options: TransferOptions,
339    ) -> Result<TransferCompleteNotification> {
340        let src_handle = self
341            .resolve_remote_handle(remote_leader_id, remote_rank, src_logical)
342            .ok_or_else(|| {
343                anyhow::anyhow!(
344                    "No mapping for remote ({}, rank {}, {:?})",
345                    remote_leader_id,
346                    remote_rank,
347                    src_logical
348                )
349            })?;
350
351        let src = RemoteDescriptor::Layout {
352            handle: src_handle,
353            block_ids: src_block_ids,
354        };
355        self.inner
356            .execute_remote_onboard(src, dst_logical, dst_block_ids, options)
357    }
358}
359
360impl ObjectBlockOps for CoordinatedWorker {
361    fn has_blocks(
362        &self,
363        keys: Vec<SequenceHash>,
364    ) -> BoxFuture<'static, Vec<(SequenceHash, Option<usize>)>> {
365        // Delegate to inner worker - Worker trait now extends ObjectBlockOps
366        self.inner.has_blocks(keys)
367    }
368
369    fn put_blocks(
370        &self,
371        keys: Vec<SequenceHash>,
372        src_layout: LogicalLayoutHandle,
373        block_ids: Vec<BlockId>,
374    ) -> BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
375        // Delegate to inner worker - inner worker resolves logical handle
376        self.inner.put_blocks(keys, src_layout, block_ids)
377    }
378
379    fn get_blocks(
380        &self,
381        keys: Vec<SequenceHash>,
382        dst_layout: LogicalLayoutHandle,
383        block_ids: Vec<BlockId>,
384    ) -> BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
385        // Delegate to inner worker - inner worker resolves logical handle
386        self.inner.get_blocks(keys, dst_layout, block_ids)
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    // TODO: Add tests with mock Worker implementation
393}