Skip to main content

kvbm_physical/manager/
mod.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transport manager for local and remote physical layouts with transfer execution.
5
6mod handle;
7mod local;
8mod metadata;
9mod remote;
10
11pub use handle::LayoutHandle;
12pub use metadata::{LogicalLayoutDescriptor, SerializedLayout, WorkerAddress};
13
14pub(crate) use local::LocalLayout;
15pub(crate) use metadata::LocalLayoutDescriptor;
16pub(crate) use remote::RemoteLayout;
17
18use crate::layout::PhysicalLayout;
19use crate::transfer::BounceBufferInternal;
20use crate::transfer::TransferContext;
21use crate::transfer::context::TransferCompleteNotification;
22use crate::transfer::executor::TransferOptionsInternal;
23use crate::transfer::options::TransferOptions;
24use crate::{BlockId, SequenceHash};
25use anyhow::{Result, anyhow, bail};
26use dynamo_memory::StorageKind;
27use dynamo_memory::nixl::NixlAgent;
28use kvbm_common::LogicalLayoutHandle;
29use std::collections::{HashMap, HashSet};
30use std::sync::atomic::{AtomicU16, Ordering};
31use std::sync::{Arc, RwLock};
32
33/// Public entry point for layout and transfer management.
34///
35/// TransferManager combines layout registration/metadata management with
36/// transfer execution capabilities, providing a unified API for:
37/// - Registering local layouts and obtaining handles
38/// - Exporting/importing layout metadata for remote workers
39/// - Executing transfers between layouts using handles
40/// - Managing CUDA, NIXL, and other execution resources
41#[derive(Clone)]
42pub struct TransferManager {
43    registry: Arc<RwLock<LayoutRegistry>>,
44    context: Arc<TransferContext>,
45}
46
47impl TransferManager {
48    /// Create a new TransferManager builder.
49    ///
50    /// The builder configures the worker ID, NIXL agent, CUDA device,
51    /// and other execution parameters before creating the manager.
52    ///
53    /// # Example
54    /// ```ignore
55    /// let manager = TransferManager::builder()
56    ///     .worker_id(0)  // NIXL agent name defaults to "worker-0"
57    ///     .nixl_backend("ucx")  // Optional: defaults to UCX from env
58    ///     .cuda_device_id(0)
59    ///     .build()?;
60    ///
61    /// // Or with custom agent name:
62    /// let manager = TransferManager::builder()
63    ///     .worker_id(0)
64    ///     .nixl_agent_name("custom-agent")
65    ///     .build()?;
66    /// ```
67    pub fn builder() -> crate::transfer::context::TransferConfigBuilder {
68        TransferContext::builder()
69    }
70
71    /// Create a TransferManager from a built TransferContext.
72    ///
73    /// This is used internally by the builder to wrap the context
74    /// and create the associated registry.
75    pub(crate) fn from_context(context: TransferContext) -> Self {
76        let worker_id = context.worker_id();
77        let nixl_agent = context.nixl_agent().clone();
78        let registry = Arc::new(RwLock::new(LayoutRegistry::new(nixl_agent, worker_id)));
79
80        Self {
81            registry,
82            context: Arc::new(context),
83        }
84    }
85
86    // ===== Layout Registration and Metadata Management =====
87
88    /// Register a local physical layout and return a unique handle.
89    ///
90    /// This registers the layout with the embedded memory manager, assigning
91    /// it a unique handle that can be used for handle-based transfers.
92    ///
93    /// # Arguments
94    /// * `layout` - Physical layout to register
95    ///
96    /// # Returns
97    /// Unique handle for the registered layout
98    ///
99    /// # Errors
100    /// Returns an error if layout IDs are exhausted (u16::MAX reached)
101    pub fn register_layout(&self, layout: PhysicalLayout) -> Result<LayoutHandle> {
102        self.registry.write().unwrap().register_local(layout)
103    }
104
105    /// Export layout metadata for transmission to remote workers.
106    ///
107    /// This exports all registered local layouts along with NIXL metadata
108    /// needed for remote memory registration.
109    ///
110    /// # Returns
111    /// Packed metadata ready for transmission to remote workers
112    pub fn export_metadata(&self) -> Result<SerializedLayout> {
113        self.registry.read().unwrap().export_metadata()
114    }
115
116    /// Import remote layout metadata.
117    ///
118    /// This loads NIXL metadata and reconstructs physical layouts from a remote
119    /// worker's exported metadata.
120    ///
121    /// # Arguments
122    /// * `metadata` - Packed metadata from remote worker
123    ///
124    /// # Returns
125    /// Vector of handles for the imported remote layouts
126    ///
127    /// # Errors
128    /// Returns an error if the remote worker was already loaded or if metadata
129    /// loading/reconstruction fails
130    pub fn import_metadata(&self, metadata: SerializedLayout) -> Result<Vec<LayoutHandle>> {
131        self.registry.write().unwrap().import_metadata(metadata)
132    }
133
134    /// Build a logical layout descriptor for a specific handle.
135    ///
136    /// This creates a descriptor that includes the logical layout type (G1, G2, G3, G4)
137    /// for use in RDMA metadata exchange. The caller must provide the logical type
138    /// mapping since only the caller (e.g., DirectWorker) knows which handle corresponds
139    /// to which logical tier.
140    ///
141    /// # Arguments
142    /// * `handle` - Handle to the local layout
143    /// * `logical_type` - The logical tier (G1, G2, G3, G4) this handle represents
144    ///
145    /// # Returns
146    /// A LogicalLayoutDescriptor ready for serialization
147    ///
148    /// # Errors
149    /// Returns an error if the handle is not found or serialization fails
150    pub fn build_logical_descriptor(
151        &self,
152        handle: LayoutHandle,
153        logical_type: LogicalLayoutHandle,
154    ) -> Result<LogicalLayoutDescriptor> {
155        self.registry
156            .read()
157            .unwrap()
158            .build_logical_descriptor(handle, logical_type)
159    }
160
161    /// Get the NIXL metadata for this worker.
162    ///
163    /// Returns the raw NIXL metadata bytes needed for remote registration.
164    pub fn get_nixl_metadata(&self) -> Result<Vec<u8>> {
165        self.registry.read().unwrap().get_nixl_metadata()
166    }
167
168    /// Get the worker address for this manager.
169    pub fn worker_address(&self) -> WorkerAddress {
170        self.registry.read().unwrap().worker_address()
171    }
172
173    /// Get a reference to the NIXL agent.
174    ///
175    /// This is useful for building layouts that need to register memory
176    /// with the same agent that the TransferManager uses.
177    pub fn nixl_agent(&self) -> &NixlAgent {
178        self.context.nixl_agent()
179    }
180
181    /// Get the layout configuration for a registered layout.
182    ///
183    /// Returns a clone of the layout's configuration, which includes
184    /// dimensions like num_blocks, num_layers, page_size, etc.
185    ///
186    /// # Arguments
187    /// * `handle` - Handle to a registered layout (local or remote)
188    ///
189    /// # Returns
190    /// A clone of the layout's configuration
191    ///
192    /// # Errors
193    /// Returns an error if the handle is not found
194    pub fn get_layout_config(&self, handle: LayoutHandle) -> Result<crate::layout::LayoutConfig> {
195        let registry = self.registry.read().unwrap();
196        let physical_layout = registry
197            .get_layout(handle)
198            .ok_or_else(|| anyhow!("invalid handle: {}", handle))?;
199        Ok(physical_layout.layout().config().clone())
200    }
201
202    // ===== Handle-Based Transfer API =====
203
204    /// Transfer complete blocks between layouts using handles.
205    ///
206    /// This function copies entire blocks (all layers and outer dimensions) between
207    /// the source and destination layouts identified by their handles. The transfer
208    /// strategy (memcpy, CUDA, NIXL) is automatically selected based on storage locations.
209    ///
210    /// The lock on the registry is held only briefly during layout lookup,
211    /// then released before executing the actual transfer.
212    ///
213    /// # Arguments
214    /// * `src_handle` - Handle to source layout
215    /// * `src_blocks` - Source block IDs to transfer
216    /// * `dst_handle` - Handle to destination layout
217    /// * `dst_blocks` - Destination block IDs to transfer
218    ///
219    /// # Returns
220    /// A notification handle that can be awaited for transfer completion
221    ///
222    /// # Errors
223    /// Returns an error if:
224    /// - Either handle is invalid
225    /// - Block IDs are out of bounds
226    /// - Transfer execution fails
227    pub fn execute_transfer(
228        &self,
229        src_handle: LayoutHandle,
230        src_blocks: &[BlockId],
231        dst_handle: LayoutHandle,
232        dst_blocks: &[BlockId],
233        options: TransferOptions,
234    ) -> Result<TransferCompleteNotification> {
235        // Clone layouts inside the lock, then drop lock before transfer
236        let (src_layout, dst_layout) = {
237            let registry = self.registry.read().unwrap();
238            let src = registry
239                .get_layout(src_handle)
240                .ok_or_else(|| anyhow!("invalid source handle: {}", src_handle))?
241                .clone(); // Cheap: just Arc refcount bump
242            let dst = registry
243                .get_layout(dst_handle)
244                .ok_or_else(|| anyhow!("invalid destination handle: {}", dst_handle))?
245                .clone();
246            (src, dst)
247        }; // Lock released here
248
249        let (
250            layer_range,
251            nixl_write_notification,
252            bounce_buffer,
253            cuda_stream,
254            src_kv_layout,
255            dst_kv_layout,
256        ) = options.dissolve();
257
258        let mut internal_options = TransferOptionsInternal::builder();
259
260        if let Some(range) = layer_range {
261            internal_options = internal_options.layer_range(range);
262        }
263
264        if let Some(notification) = nixl_write_notification {
265            internal_options = internal_options.nixl_write_notification(notification);
266        }
267
268        if let Some(bounce) = bounce_buffer {
269            let (handle, block_ids) = bounce.into_parts();
270            let bounce_buffer = self.create_bounce_buffer(handle, block_ids)?;
271            internal_options = internal_options.bounce_buffer(bounce_buffer);
272        }
273
274        if let Some(stream) = cuda_stream {
275            internal_options = internal_options.cuda_stream(stream);
276        }
277
278        if let Some(layout) = src_kv_layout {
279            internal_options = internal_options.src_kv_layout(layout);
280        }
281
282        if let Some(layout) = dst_kv_layout {
283            internal_options = internal_options.dst_kv_layout(layout);
284        }
285
286        let options = internal_options.build()?;
287
288        tracing::debug!(
289            src_handle = src_handle.to_string(),
290            dst_handle = dst_handle.to_string(),
291            "Executing transfer; src_blocks = {:?}; dst_blocks = {:?}",
292            src_blocks,
293            dst_blocks,
294        );
295
296        // Execute transfer with no lock held
297        super::transfer::executor::execute_transfer(
298            &src_layout,
299            &dst_layout,
300            src_blocks,
301            dst_blocks,
302            options,
303            &self.context,
304        )
305    }
306
307    /// Execute a G4 offload.
308    ///
309    /// Takes a LayoutHandle and a vector of block IDs for the source blocks and
310    /// a list of SequenceHashes for the destination blocks.
311    ///
312    /// use an extension on TransferOptions to pass in the "rank/part" of the the object in a
313    /// multi-worker/multi-tp scenario.
314    pub fn execute_g4_offload(
315        _src_handle: LayoutHandle,
316        _src_blocks: &[BlockId],
317        _dst_object: &[SequenceHash],
318        _options: TransferOptions, // add rank/part to the options
319    ) -> Result<TransferCompleteNotification> {
320        // check registration cache for the remote object, if it's not found, register it with nixl
321        // register all non-registered blocks with nixl in parallel
322        // then extend super::transfer::executor to access the memory regions for the source
323        // and generate a nixl descriptor
324        todo!("implement remote offload")
325    }
326
327    pub fn execute_g4_onboard() {
328        todo!("implement remote onboard")
329    }
330
331    // ===== Query Methods =====
332
333    /// Get the worker ID for this manager.
334    pub fn worker_id(&self) -> u64 {
335        self.context.worker_id()
336    }
337
338    /// Get handles for all locally registered layouts.
339    pub fn get_local_handles(&self) -> Vec<LayoutHandle> {
340        self.registry.read().unwrap().local_handles()
341    }
342
343    /// Get handles for all imported remote layouts.
344    pub fn get_remote_handles(&self) -> Vec<LayoutHandle> {
345        self.registry.read().unwrap().remote_handles()
346    }
347
348    /// Get a clone of the physical layout for a given handle.
349    ///
350    /// # Arguments
351    /// * `handle` - Handle to a registered layout (local or remote)
352    ///
353    /// # Returns
354    /// A clone of the physical layout, or None if the handle is not found.
355    pub fn get_physical_layout(&self, handle: LayoutHandle) -> Option<PhysicalLayout> {
356        self.registry.read().unwrap().get_layout(handle).cloned()
357    }
358
359    /// Create a bounce buffer specification from a layout handle and block IDs.
360    ///
361    /// This resolves the layout handle to a physical layout and wraps it in a
362    /// BounceBufferSpec implementation for use in transfer options.
363    pub(crate) fn create_bounce_buffer(
364        &self,
365        handle: LayoutHandle,
366        block_ids: Vec<BlockId>,
367    ) -> Result<BounceBufferInternal> {
368        let layout = {
369            let registry = self.registry.read().unwrap();
370            registry
371                .get_layout(handle)
372                .ok_or_else(|| anyhow!("invalid bounce buffer handle: {}", handle))?
373                .clone()
374        };
375
376        Ok(BounceBufferInternal::from_layout(layout, block_ids))
377    }
378
379    // ===== Internal Methods for Testing =====
380
381    /// Get the internal transfer context.
382    #[doc(hidden)]
383    pub fn context(&self) -> &TransferContext {
384        &self.context
385    }
386
387    /// Get access to the internal layout registry.
388    ///
389    /// This is primarily for testing utilities that need direct layout access
390    /// (e.g., fill patterns, checksum computation).
391    #[doc(hidden)]
392    pub fn registry(&self) -> &RwLock<LayoutRegistry> {
393        &self.registry
394    }
395
396    /// Get the H2D stream (for testing only).
397    #[cfg(test)]
398    #[allow(dead_code)]
399    pub(crate) fn h2d_stream(&self) -> &std::sync::Arc<cudarc::driver::CudaStream> {
400        self.context.h2d_stream()
401    }
402
403    /// Get the D2H stream (for testing only).
404    #[cfg(test)]
405    #[allow(dead_code)]
406    pub(crate) fn d2h_stream(&self) -> &std::sync::Arc<cudarc::driver::CudaStream> {
407        self.context.d2h_stream()
408    }
409
410    /// Get the CUDA context (for testing only).
411    #[cfg(test)]
412    #[allow(dead_code)]
413    pub(crate) fn cuda_context(&self) -> &std::sync::Arc<cudarc::driver::CudaContext> {
414        self.context.cuda_context()
415    }
416
417    /// Register a CUDA event for completion (for testing only).
418    #[cfg(test)]
419    #[allow(dead_code)]
420    pub(crate) fn register_cuda_event(
421        &self,
422        event: cudarc::driver::CudaEvent,
423    ) -> TransferCompleteNotification {
424        self.context.register_cuda_event(event)
425    }
426
427    /// Get the CUDA memory pool (for testing only).
428    #[cfg(test)]
429    #[expect(dead_code)]
430    pub(crate) fn cuda_pool(&self) -> &std::sync::Arc<dynamo_memory::CudaMemPool> {
431        self.context.cuda_pool()
432    }
433}
434
435/// Internal registry for local and remote physical layouts with NIXL integration.
436///
437/// The LayoutRegistry handles:
438/// - Registering local layouts with unique handles
439/// - Exporting local layout metadata for remote access
440/// - Importing remote layout metadata and reconstructing layouts
441/// - Managing NIXL metadata for RDMA operations
442#[derive(Debug)]
443#[doc(hidden)]
444pub struct LayoutRegistry {
445    /// NIXL agent for memory registration
446    nixl_agent: NixlAgent,
447    /// Worker ID for this manager
448    worker_id: u64,
449    /// Next layout ID to assign (monotonically increasing)
450    next_layout_id: AtomicU16,
451    /// Local layouts registered on this worker
452    local_layouts: HashMap<LayoutHandle, LocalLayout>,
453    /// Remote layouts imported from other workers
454    remote_layouts: HashMap<LayoutHandle, RemoteLayout>,
455    /// Set of loaded remote workers (agent_name, worker_id) to prevent duplicates
456    loaded_remotes: HashSet<(String, u64)>,
457}
458
459#[expect(dead_code)]
460impl LayoutRegistry {
461    /// Create a new layout manager.
462    ///
463    /// # Arguments
464    /// * `nixl_agent` - NIXL agent for memory registration
465    /// * `worker_id` - Unique identifier for this worker
466    pub(crate) fn new(nixl_agent: NixlAgent, worker_id: u64) -> Self {
467        Self {
468            nixl_agent,
469            worker_id,
470            next_layout_id: AtomicU16::new(0),
471            local_layouts: HashMap::new(),
472            remote_layouts: HashMap::new(),
473            loaded_remotes: HashSet::new(),
474        }
475    }
476
477    /// Register a local physical layout.
478    ///
479    /// # Arguments
480    /// * `layout` - Physical layout to register
481    ///
482    /// # Returns
483    /// Unique handle for the registered layout
484    ///
485    /// # Errors
486    /// Returns an error if layout IDs are exhausted (u16::MAX reached)
487    pub(crate) fn register_local(&mut self, layout: PhysicalLayout) -> Result<LayoutHandle> {
488        // Check before incrementing to prevent wrapping
489        let current = self.next_layout_id.load(Ordering::SeqCst);
490        if current == u16::MAX {
491            bail!(
492                "Layout ID overflow: maximum number of layouts ({}) reached",
493                u16::MAX
494            );
495        }
496        let layout_id = self.next_layout_id.fetch_add(1, Ordering::SeqCst);
497
498        // Create handle
499        let handle = LayoutHandle::new(self.worker_id, layout_id);
500
501        // Wrap in LocalLayout
502        let local_layout = LocalLayout::new(handle, layout);
503
504        // Store
505        self.local_layouts.insert(handle, local_layout);
506
507        Ok(handle)
508    }
509
510    /// Export local layout metadata for transmission to remote workers.
511    ///
512    /// This exports:
513    /// - NIXL agent metadata for remote memory registration
514    /// - All host and device layouts (disk layouts are excluded)
515    /// - Worker address information
516    ///
517    /// # Returns
518    /// Packed metadata ready for transmission
519    pub(crate) fn export_metadata(&self) -> Result<SerializedLayout> {
520        // Get NIXL metadata from agent
521        let nixl_metadata = self
522            .nixl_agent
523            .get_local_md()
524            .map_err(|e| anyhow!("failed to get NIXL local metadata: {:?}", e))?;
525
526        // Create worker address
527        let worker_address = WorkerAddress::new(self.worker_id, self.nixl_agent.name().to_string());
528
529        // Filter and serialize layouts (only host and device, skip disk)
530        let mut serialized_layouts = Vec::new();
531        for (handle, local_layout) in &self.local_layouts {
532            let location = local_layout.layout().location();
533
534            // Only export host and device layouts
535            if matches!(
536                location,
537                StorageKind::System | StorageKind::Device(_) | StorageKind::Pinned
538            ) {
539                let serialized = local_layout
540                    .layout()
541                    .to_descriptor()
542                    .map_err(|e| anyhow!("failed to serialize layout {}: {}", handle, e))?;
543
544                serialized_layouts.push(LocalLayoutDescriptor::new_with_default_type(
545                    *handle, serialized,
546                ));
547            }
548        }
549
550        // Pack into managed metadata
551        SerializedLayout::pack(worker_address, nixl_metadata, serialized_layouts)
552    }
553
554    /// Import remote layout metadata.
555    ///
556    /// This:
557    /// - Validates the remote worker hasn't been loaded already
558    /// - Loads NIXL metadata into the agent
559    /// - Reconstructs physical layouts from serialized data
560    /// - Stores them as remote layouts
561    ///
562    /// # Arguments
563    /// * `metadata` - Packed metadata from remote worker
564    ///
565    /// # Returns
566    /// Vector of handles for the imported layouts
567    ///
568    /// # Errors
569    /// Returns an error if:
570    /// - The remote worker was already loaded
571    /// - NIXL metadata loading fails
572    /// - Agent name mismatch after loading
573    /// - Layout reconstruction fails
574    pub(crate) fn import_metadata(
575        &mut self,
576        metadata: SerializedLayout,
577    ) -> Result<Vec<LayoutHandle>> {
578        // Unpack metadata
579        let inner = metadata.unpack()?;
580
581        // Validate not already loaded
582        let remote_key = (
583            inner.worker_address.nixl_agent_name.clone(),
584            inner.worker_address.worker_id,
585        );
586        if self.loaded_remotes.contains(&remote_key) {
587            bail!(
588                "Remote worker already loaded: {} (worker_id={})",
589                remote_key.0,
590                remote_key.1
591            );
592        }
593
594        // Load NIXL metadata
595        let returned_agent_name = self
596            .nixl_agent
597            .load_remote_md(&inner.nixl_metadata)
598            .map_err(|e| anyhow!("failed to load remote NIXL metadata: {:?}", e))?;
599
600        // Verify agent name matches
601        if returned_agent_name != inner.worker_address.nixl_agent_name {
602            bail!(
603                "Agent name mismatch: expected '{}', got '{}'",
604                inner.worker_address.nixl_agent_name,
605                returned_agent_name
606            );
607        }
608
609        // Reconstruct layouts
610        let mut imported_handles = Vec::new();
611        for serialized_with_handle in inner.layouts {
612            let handle = serialized_with_handle.handle;
613            let layout = PhysicalLayout::from_descriptor(serialized_with_handle.layout)
614                .map_err(|e| anyhow!("failed to reconstruct layout {}: {}", handle, e))?;
615
616            let remote_layout = RemoteLayout::new(handle, layout);
617            self.remote_layouts.insert(handle, remote_layout);
618            imported_handles.push(handle);
619        }
620
621        // Mark remote as loaded
622        self.loaded_remotes.insert(remote_key);
623
624        Ok(imported_handles)
625    }
626
627    /// Build a logical layout descriptor for a specific handle.
628    ///
629    /// # Arguments
630    /// * `handle` - Handle to the local layout
631    /// * `logical_type` - The logical tier (G1, G2, G3, G4) this handle represents
632    ///
633    /// # Returns
634    /// A LogicalLayoutDescriptor ready for serialization
635    pub(crate) fn build_logical_descriptor(
636        &self,
637        handle: LayoutHandle,
638        logical_type: LogicalLayoutHandle,
639    ) -> Result<LogicalLayoutDescriptor> {
640        let local_layout = self
641            .local_layouts
642            .get(&handle)
643            .ok_or_else(|| anyhow!("Layout handle not found: {:?}", handle))?;
644
645        let layout_descriptor = local_layout
646            .layout()
647            .to_descriptor()
648            .map_err(|e| anyhow!("failed to serialize layout {}: {}", handle, e))?;
649
650        Ok(LogicalLayoutDescriptor::new(
651            handle,
652            logical_type,
653            layout_descriptor,
654        ))
655    }
656
657    /// Get the NIXL metadata for this worker.
658    pub(crate) fn get_nixl_metadata(&self) -> Result<Vec<u8>> {
659        self.nixl_agent
660            .get_local_md()
661            .map_err(|e| anyhow!("failed to get NIXL local metadata: {:?}", e))
662    }
663
664    /// Get the worker address for this registry.
665    pub(crate) fn worker_address(&self) -> WorkerAddress {
666        WorkerAddress::new(self.worker_id, self.nixl_agent.name().to_string())
667    }
668
669    /// Get a local layout by handle.
670    pub(crate) fn get_local(&self, handle: LayoutHandle) -> Option<&LocalLayout> {
671        self.local_layouts.get(&handle)
672    }
673
674    /// Get a remote layout by handle.
675    pub(crate) fn get_remote(&self, handle: LayoutHandle) -> Option<&RemoteLayout> {
676        self.remote_layouts.get(&handle)
677    }
678
679    /// Get a layout by handle (either local or remote).
680    ///
681    /// # Returns
682    /// Returns a reference to the PhysicalLayout if found
683    pub fn get_layout(&self, handle: LayoutHandle) -> Option<&PhysicalLayout> {
684        self.local_layouts
685            .get(&handle)
686            .map(|l| l.layout())
687            .or_else(|| self.remote_layouts.get(&handle).map(|r| r.layout()))
688    }
689
690    /// Check if a handle refers to a local layout.
691    pub(crate) fn is_local(&self, handle: LayoutHandle) -> bool {
692        self.local_layouts.contains_key(&handle)
693    }
694
695    /// Check if a handle refers to a remote layout.
696    pub(crate) fn is_remote(&self, handle: LayoutHandle) -> bool {
697        self.remote_layouts.contains_key(&handle)
698    }
699
700    /// Get the number of local layouts.
701    pub(crate) fn local_count(&self) -> usize {
702        self.local_layouts.len()
703    }
704
705    /// Get the number of remote layouts.
706    pub(crate) fn remote_count(&self) -> usize {
707        self.remote_layouts.len()
708    }
709
710    /// Get the worker ID for this manager.
711    pub(crate) fn worker_id(&self) -> u64 {
712        self.worker_id
713    }
714
715    /// Get all local layout handles.
716    pub(crate) fn local_handles(&self) -> Vec<LayoutHandle> {
717        self.local_layouts.keys().copied().collect()
718    }
719
720    /// Get all remote layout handles.
721    pub(crate) fn remote_handles(&self) -> Vec<LayoutHandle> {
722        self.remote_layouts.keys().copied().collect()
723    }
724}
725
726#[cfg(all(test, feature = "testing-kvbm"))]
727mod tests {
728    use super::*;
729    use crate::layout::LayoutConfig;
730    use dynamo_memory::nixl::NixlAgent;
731
732    fn make_test_agent(name: &str) -> NixlAgent {
733        NixlAgent::new(name).expect("failed to create agent")
734    }
735
736    fn make_test_layout(agent: &NixlAgent) -> PhysicalLayout {
737        let config = LayoutConfig::builder()
738            .num_blocks(2)
739            .num_layers(2)
740            .outer_dim(2)
741            .page_size(4)
742            .inner_dim(8)
743            .dtype_width_bytes(2)
744            .build()
745            .unwrap();
746
747        PhysicalLayout::builder(agent.clone())
748            .with_config(config)
749            .fully_contiguous()
750            .allocate_system()
751            .build()
752            .unwrap()
753    }
754
755    #[test]
756    fn test_manager_creation() {
757        let agent = make_test_agent("test-manager");
758        let manager = LayoutRegistry::new(agent, 42);
759
760        assert_eq!(manager.worker_id(), 42);
761        assert_eq!(manager.local_count(), 0);
762        assert_eq!(manager.remote_count(), 0);
763    }
764
765    #[test]
766    fn test_register_local() {
767        let agent = make_test_agent("test-register");
768        let mut manager = LayoutRegistry::new(agent.clone(), 100);
769
770        let layout = make_test_layout(&agent);
771        let handle = manager.register_local(layout).unwrap();
772
773        assert_eq!(handle.worker_id(), 100);
774        assert_eq!(handle.layout_id(), 0);
775        assert_eq!(manager.local_count(), 1);
776        assert!(manager.is_local(handle));
777        assert!(!manager.is_remote(handle));
778    }
779
780    #[test]
781    fn test_register_multiple_locals() {
782        let agent = make_test_agent("test-multiple");
783        let mut manager = LayoutRegistry::new(agent.clone(), 1);
784
785        let handle1 = manager.register_local(make_test_layout(&agent)).unwrap();
786        let handle2 = manager.register_local(make_test_layout(&agent)).unwrap();
787        let handle3 = manager.register_local(make_test_layout(&agent)).unwrap();
788
789        assert_eq!(handle1.layout_id(), 0);
790        assert_eq!(handle2.layout_id(), 1);
791        assert_eq!(handle3.layout_id(), 2);
792        assert_eq!(manager.local_count(), 3);
793    }
794
795    #[test]
796    #[ignore] // Requires actual NIXL memory registration
797    fn test_export_import_roundtrip() {
798        // Create source manager and register layouts
799        let source_agent = make_test_agent("source");
800        let mut source_manager = LayoutRegistry::new(source_agent.clone(), 1);
801
802        let handle1 = source_manager
803            .register_local(make_test_layout(&source_agent))
804            .unwrap();
805        let handle2 = source_manager
806            .register_local(make_test_layout(&source_agent))
807            .unwrap();
808
809        // Export metadata
810        let metadata = source_manager.export_metadata().unwrap();
811        assert!(!metadata.is_empty());
812
813        // Create destination manager and import
814        let dest_agent = make_test_agent("dest");
815        let mut dest_manager = LayoutRegistry::new(dest_agent, 2);
816
817        let imported_handles = dest_manager.import_metadata(metadata).unwrap();
818
819        // Verify
820        assert_eq!(imported_handles.len(), 2);
821        assert_eq!(dest_manager.remote_count(), 2);
822        assert!(dest_manager.is_remote(handle1));
823        assert!(dest_manager.is_remote(handle2));
824
825        // Can get layouts
826        assert!(dest_manager.get_remote(handle1).is_some());
827        assert!(dest_manager.get_remote(handle2).is_some());
828        assert!(dest_manager.get_layout(handle1).is_some());
829    }
830
831    #[test]
832    #[ignore] // Requires actual NIXL memory registration
833    fn test_import_duplicate_remote_fails() {
834        let source_agent = make_test_agent("source2");
835        let mut source_manager = LayoutRegistry::new(source_agent.clone(), 10);
836
837        source_manager
838            .register_local(make_test_layout(&source_agent))
839            .unwrap();
840
841        let metadata = source_manager.export_metadata().unwrap();
842
843        let dest_agent = make_test_agent("dest2");
844        let mut dest_manager = LayoutRegistry::new(dest_agent, 20);
845
846        // First import succeeds
847        let metadata_clone = SerializedLayout::from_bytes(metadata.as_bytes().to_vec());
848        dest_manager.import_metadata(metadata).unwrap();
849
850        // Second import should fail
851        let result = dest_manager.import_metadata(metadata_clone);
852        assert!(result.is_err());
853        assert!(result.unwrap_err().to_string().contains("already loaded"));
854    }
855
856    #[test]
857    fn test_get_layout_handles() {
858        let agent = make_test_agent("test-handles");
859        let mut manager = LayoutRegistry::new(agent.clone(), 5);
860
861        let h1 = manager.register_local(make_test_layout(&agent)).unwrap();
862        let h2 = manager.register_local(make_test_layout(&agent)).unwrap();
863
864        let handles = manager.local_handles();
865        assert_eq!(handles.len(), 2);
866        assert!(handles.contains(&h1));
867        assert!(handles.contains(&h2));
868    }
869}