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}