Skip to main content

kvbm_physical/transfer/
context.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transfer context.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicUsize, Ordering};
8
9use anyhow::Result;
10use cudarc::driver::{CudaContext, CudaEvent, CudaStream};
11use derive_builder::Builder;
12use tokio::sync::mpsc;
13use uuid::Uuid;
14
15use dynamo_memory::CudaMemPool;
16use dynamo_memory::nixl::{NixlAgent, NixlBackendConfig, XferRequest};
17use velo::EventManager;
18
19use crate::manager::TransferManager;
20
21// Notifications module is declared in ../mod.rs
22// Re-export for convenience
23use super::TransferCapabilities;
24use notifications::RegisterPollingNotification;
25
26pub(crate) use super::notifications;
27pub use super::notifications::TransferCompleteNotification;
28
29#[derive(Clone, Builder)]
30#[builder(pattern = "owned", build_fn(private, name = "build_internal"), public)]
31#[allow(dead_code)] // Fields are used in build() but derive macros confuse dead code analysis
32pub struct TransferConfig {
33    #[builder(default = "Arc::new(EventManager::local())")]
34    event_system: Arc<EventManager>,
35
36    /// Optional custom name for the NIXL agent. If not provided, defaults to "worker-{worker_id}"
37    #[builder(default = "None", setter(strip_option))]
38    nixl_agent_name: Option<String>,
39
40    /// Backend configuration for NIXL backends to enable
41    #[builder(default = "NixlBackendConfig::default()")]
42    nixl_backend_config: NixlBackendConfig,
43
44    #[builder(default = "0")]
45    cuda_device_id: usize,
46
47    #[builder(default = "get_tokio_runtime()")]
48    tokio_runtime: TokioRuntime,
49
50    #[builder(default = "TransferCapabilities::default()")]
51    capabilities: TransferCapabilities,
52
53    /// Size in bytes to pre-allocate for the CUDA memory pool (default: 64 MiB)
54    #[builder(default = "64 * 1024 * 1024")]
55    cuda_pool_reserve_size: usize,
56
57    /// Release threshold for the CUDA memory pool (default: Some(64 MiB))
58    /// Memory above this threshold is returned to the system when freed.
59    /// If None, no release threshold is set.
60    #[builder(default = "Some(64 * 1024 * 1024)")]
61    cuda_pool_release_threshold: Option<u64>,
62}
63
64impl TransferConfigBuilder {
65    /// Initialize builder with event system and tokio handle.
66    ///
67    /// This sets the event_system and tokio runtime handle, ensuring consistency
68    /// with Nova's event system. Use this when the runtime has already been
69    /// constructed and you want components to share the same event notification
70    /// infrastructure.
71    pub fn from_event_system_and_handle(
72        self,
73        event_system: Arc<EventManager>,
74        handle: tokio::runtime::Handle,
75    ) -> Self {
76        self.event_system(event_system)
77            .tokio_runtime(TokioRuntime::Handle(handle))
78    }
79
80    /// Directly provide a pre-configured wrapped NIXL agent (mainly for testing).
81    ///
82    /// This bypasses the agent creation and backend initialization logic,
83    /// using the provided agent directly. Useful for tests that need full
84    /// control over agent configuration.
85    pub fn nixl_agent(self, agent: NixlAgent) -> TransferConfigBuilderWithAgent {
86        TransferConfigBuilderWithAgent {
87            builder: self,
88            agent,
89        }
90    }
91
92    /// Add a NIXL backend to enable (uses default plugin parameters).
93    pub fn nixl_backend(mut self, backend: impl Into<String>) -> Self {
94        let config = self
95            .nixl_backend_config
96            .get_or_insert_with(NixlBackendConfig::default);
97        *config = config.clone().with_backend(backend);
98        self
99    }
100
101    /// Load NIXL backend configuration from environment variables.
102    ///
103    /// This merges environment-based configuration with any backends already
104    /// configured via the builder.
105    pub fn with_env_backends(mut self) -> Result<Self> {
106        let env_config = NixlBackendConfig::from_env()?;
107        let config = self
108            .nixl_backend_config
109            .get_or_insert_with(NixlBackendConfig::default);
110        *config = config.clone().merge(env_config);
111        Ok(self)
112    }
113
114    pub fn build(self) -> Result<TransferManager> {
115        let mut config = self.build_internal()?;
116
117        let worker_id = config.event_system.system_id();
118
119        // Merge environment backends if not explicitly configured
120        if config.nixl_backend_config.backends().is_empty() {
121            config.nixl_backend_config = NixlBackendConfig::from_env()?;
122        }
123
124        // Derive agent name from worker_id if not provided
125        let agent_name = config
126            .nixl_agent_name
127            .unwrap_or_else(|| format!("worker-{}", worker_id));
128
129        let nixl_agent =
130            NixlAgent::from_nixl_backend_config(&agent_name, config.nixl_backend_config)?;
131
132        let cuda_context = CudaContext::new(config.cuda_device_id)?;
133        let context = TransferContext::new(
134            nixl_agent,
135            config.event_system,
136            cuda_context,
137            config.tokio_runtime,
138            config.capabilities,
139            config.cuda_pool_reserve_size,
140            config.cuda_pool_release_threshold,
141        )?;
142        Ok(TransferManager::from_context(context))
143    }
144}
145
146/// Builder that already has a pre-configured NIXL agent.
147///
148/// This is generally used for testing when you want to pass in an agent directly
149/// rather than having it created by the builder.
150pub struct TransferConfigBuilderWithAgent {
151    builder: TransferConfigBuilder,
152    agent: NixlAgent,
153}
154
155impl TransferConfigBuilderWithAgent {
156    /// Build the TransferManager using the pre-configured agent.
157    pub fn build(self) -> Result<TransferManager> {
158        let config = self.builder.build_internal()?;
159        let cuda_context = CudaContext::new(config.cuda_device_id)?;
160        let context = TransferContext::new(
161            self.agent,
162            config.event_system,
163            cuda_context,
164            config.tokio_runtime,
165            config.capabilities,
166            config.cuda_pool_reserve_size,
167            config.cuda_pool_release_threshold,
168        )?;
169        Ok(TransferManager::from_context(context))
170    }
171
172    pub fn cuda_device_id(mut self, cuda_device_id: usize) -> Self {
173        self.builder = self.builder.cuda_device_id(cuda_device_id);
174        self
175    }
176}
177
178fn get_tokio_runtime() -> TokioRuntime {
179    match tokio::runtime::Handle::try_current() {
180        Ok(handle) => TokioRuntime::Handle(handle),
181        Err(_) => {
182            let rt = tokio::runtime::Builder::new_multi_thread()
183                .enable_all()
184                .max_blocking_threads(4)
185                .worker_threads(2)
186                .build()
187                .expect("failed to build tokio runtime");
188
189            TokioRuntime::Shared(Arc::new(rt))
190        }
191    }
192}
193
194#[derive(Debug, Clone)]
195#[doc(hidden)]
196pub enum TokioRuntime {
197    Handle(tokio::runtime::Handle),
198    Shared(Arc<tokio::runtime::Runtime>),
199}
200
201impl TokioRuntime {
202    pub fn handle(&self) -> &tokio::runtime::Handle {
203        match self {
204            TokioRuntime::Handle(handle) => handle,
205            TokioRuntime::Shared(runtime) => runtime.handle(),
206        }
207    }
208}
209
210#[derive(Clone)]
211#[doc(hidden)]
212pub struct TransferContext {
213    worker_id: u64,
214    nixl_agent: NixlAgent,
215    #[allow(dead_code)]
216    cuda_context: Arc<CudaContext>,
217    d2h_stream: Arc<CudaStream>,
218    h2d_stream: Arc<CudaStream>,
219    d2h_streams: Vec<Arc<CudaStream>>,
220    h2d_streams: Vec<Arc<CudaStream>>,
221    current_d2h_stream: Arc<AtomicUsize>,
222    current_h2d_stream: Arc<AtomicUsize>,
223    #[allow(dead_code)]
224    tokio_runtime: TokioRuntime,
225    capabilities: TransferCapabilities,
226    event_system: Arc<EventManager>,
227    // CUDA memory pool for kernel allocations
228    cuda_pool: Arc<CudaMemPool>,
229    // Channels for background notification handlers
230    tx_nixl_status: mpsc::Sender<RegisterPollingNotification<notifications::NixlStatusChecker>>,
231    tx_cuda_event: mpsc::Sender<RegisterPollingNotification<notifications::CudaEventChecker>>,
232    #[allow(dead_code)]
233    tx_nixl_events: mpsc::Sender<notifications::RegisterNixlNotification>,
234}
235
236impl TransferContext {
237    pub fn builder() -> TransferConfigBuilder {
238        TransferConfigBuilder::default()
239    }
240
241    pub(crate) fn new(
242        nixl_agent: NixlAgent,
243        event_system: Arc<EventManager>,
244        cuda_context: Arc<CudaContext>,
245        tokio_runtime: TokioRuntime,
246        capabilities: TransferCapabilities,
247        cuda_pool_reserve_size: usize,
248        cuda_pool_release_threshold: Option<u64>,
249    ) -> Result<Self> {
250        unsafe { cuda_context.disable_event_tracking() };
251
252        // Create CUDA memory pool for kernel allocations
253        let mut pool_builder = CudaMemPool::builder(cuda_context.clone(), cuda_pool_reserve_size);
254        if let Some(threshold) = cuda_pool_release_threshold {
255            pool_builder = pool_builder.release_threshold(threshold);
256        }
257        let cuda_pool = Arc::new(pool_builder.build()?);
258
259        // Create channels for background notification handlers
260        let (tx_nixl_status, rx_nixl_status) = mpsc::channel(64);
261        let (tx_cuda_event, rx_cuda_event) = mpsc::channel(64);
262        let (tx_nixl_events, rx_nixl_events) = mpsc::channel(64);
263
264        // Spawn background handlers
265        let handle = tokio_runtime.handle();
266
267        // Spawn NIXL status polling handler
268        handle.spawn(notifications::process_polling_notifications(
269            rx_nixl_status,
270            event_system.clone(),
271        ));
272
273        // Spawn CUDA event polling handler
274        handle.spawn(notifications::process_polling_notifications(
275            rx_cuda_event,
276            event_system.clone(),
277        ));
278
279        // Spawn NIXL notification events handler
280        handle.spawn(notifications::process_nixl_notification_events(
281            nixl_agent.raw_agent().clone(),
282            rx_nixl_events,
283            event_system.clone(),
284        ));
285
286        let d2h_streams: Vec<Arc<CudaStream>> = (0..4)
287            .map(|_| cuda_context.new_stream())
288            .collect::<Result<Vec<_>, _>>()?;
289
290        let h2d_streams: Vec<Arc<CudaStream>> = (0..4)
291            .map(|_| cuda_context.new_stream())
292            .collect::<Result<Vec<_>, _>>()?;
293
294        let d2h_stream = d2h_streams[0].clone();
295        let h2d_stream = h2d_streams[0].clone();
296
297        let current_d2h_stream = Arc::new(AtomicUsize::new(0));
298        let current_h2d_stream = Arc::new(AtomicUsize::new(0));
299
300        Ok(Self {
301            worker_id: event_system.system_id(),
302            nixl_agent,
303            cuda_context: cuda_context.clone(),
304            d2h_stream,
305            h2d_stream,
306            d2h_streams,
307            h2d_streams,
308            current_d2h_stream,
309            current_h2d_stream,
310            tokio_runtime,
311            capabilities,
312            event_system,
313            cuda_pool,
314            tx_nixl_status,
315            tx_cuda_event,
316            tx_nixl_events,
317        })
318    }
319
320    pub(crate) fn nixl_agent(&self) -> &NixlAgent {
321        &self.nixl_agent
322    }
323
324    #[allow(dead_code)]
325    pub(crate) fn cuda_context(&self) -> &Arc<CudaContext> {
326        &self.cuda_context
327    }
328
329    // Provides the same d2h stream per invocation
330    #[allow(dead_code)]
331    pub(crate) fn d2h_stream(&self) -> &Arc<CudaStream> {
332        &self.d2h_stream
333    }
334
335    // Provides the same h2d stream per invocation
336    #[allow(dead_code)]
337    pub(crate) fn h2d_stream(&self) -> &Arc<CudaStream> {
338        &self.h2d_stream
339    }
340
341    // Provides the next d2h stream in a round-robin fashion
342    pub(crate) fn next_d2h_streams(&self) -> Arc<CudaStream> {
343        let current_d2h_stream = self.current_d2h_stream.fetch_add(1, Ordering::Relaxed);
344        self.d2h_streams[current_d2h_stream % self.d2h_streams.len()].clone()
345    }
346
347    // Provides the next h2d stream in a round-robin fashion
348    pub(crate) fn next_h2d_streams(&self) -> Arc<CudaStream> {
349        let current_h2d_stream = self.current_h2d_stream.fetch_add(1, Ordering::Relaxed);
350        self.h2d_streams[current_h2d_stream % self.h2d_streams.len()].clone()
351    }
352
353    /// Acquire an H2D stream for use by caller.
354    ///
355    /// This returns a stream from the pool that the caller can use for multiple
356    /// sequential operations. The caller is responsible for all synchronization
357    /// (e.g., recording events after operations).
358    ///
359    /// Used for layer-wise transfers where all layers must execute on the same stream.
360    pub fn acquire_h2d_stream(&self) -> Arc<CudaStream> {
361        self.next_h2d_streams()
362    }
363
364    /// Acquire a D2H stream for use by caller.
365    ///
366    /// This returns a stream from the pool that the caller can use for multiple
367    /// sequential operations. The caller is responsible for all synchronization
368    /// (e.g., recording events after operations).
369    ///
370    /// Used for layer-wise transfers where all layers must execute on the same stream.
371    pub fn acquire_d2h_stream(&self) -> Arc<CudaStream> {
372        self.next_d2h_streams()
373    }
374
375    #[allow(dead_code)]
376    #[doc(hidden)]
377    pub fn tokio(&self) -> &tokio::runtime::Handle {
378        self.tokio_runtime.handle()
379    }
380
381    pub(crate) fn capabilities(&self) -> &TransferCapabilities {
382        &self.capabilities
383    }
384
385    #[doc(hidden)]
386    pub fn event_system(&self) -> &Arc<EventManager> {
387        &self.event_system
388    }
389
390    /// Get the CUDA memory pool for kernel allocations.
391    pub(crate) fn cuda_pool(&self) -> &Arc<CudaMemPool> {
392        &self.cuda_pool
393    }
394
395    /// Register a NIXL transfer request for status polling completion.
396    ///
397    /// This method enqueues the transfer request to be polled for completion
398    /// using `agent.get_xfer_status()`. Returns a notification object that
399    /// can be awaited for completion.
400    pub(crate) fn register_nixl_status(
401        &self,
402        xfer_req: XferRequest,
403    ) -> TransferCompleteNotification {
404        let event = self
405            .event_system
406            .new_event()
407            .expect("Failed to allocate event");
408        let handle = event.into_handle();
409        let awaiter = self
410            .event_system
411            .awaiter(handle)
412            .expect("Failed to get awaiter");
413
414        let notification = notifications::RegisterPollingNotification {
415            uuid: Uuid::new_v4(),
416            checker: notifications::NixlStatusChecker::new(
417                self.nixl_agent.raw_agent().clone(),
418                xfer_req,
419            ),
420            event_handle: handle,
421        };
422
423        // Send to background handler — log error if channel is full or closed
424        if let Err(e) = self.tx_nixl_status.try_send(notification) {
425            tracing::error!(
426                "Failed to enqueue NIXL status notification: channel full or closed: {}",
427                e
428            );
429        }
430
431        TransferCompleteNotification::from_awaiter(awaiter)
432    }
433
434    /// Register a CUDA event for polling completion.
435    ///
436    /// This method enqueues the CUDA event to be polled for completion.
437    /// Returns a notification object that can be awaited for completion.
438    pub(crate) fn register_cuda_event(&self, event: CudaEvent) -> TransferCompleteNotification {
439        let new_event = self
440            .event_system
441            .new_event()
442            .expect("Failed to allocate event");
443        let handle = new_event.into_handle();
444        let awaiter = self
445            .event_system
446            .awaiter(handle)
447            .expect("Failed to get awaiter");
448
449        let notification = notifications::RegisterPollingNotification {
450            uuid: Uuid::new_v4(),
451            checker: notifications::CudaEventChecker::new(event),
452            event_handle: handle,
453        };
454
455        // Send to background handler — log error if channel is full or closed
456        if let Err(e) = self.tx_cuda_event.try_send(notification) {
457            tracing::error!(
458                "Failed to enqueue CUDA event notification: channel full or closed: {}",
459                e
460            );
461        }
462
463        TransferCompleteNotification::from_awaiter(awaiter)
464    }
465
466    /// Register a NIXL transfer request for notification-based completion.
467    ///
468    /// This method enqueues the transfer request to be completed via NIXL
469    /// notification events. Returns a notification object that can be awaited
470    /// for completion.
471    #[allow(dead_code)]
472    pub(crate) fn register_nixl_event(
473        &self,
474        xfer_req: XferRequest,
475    ) -> TransferCompleteNotification {
476        let event = self
477            .event_system
478            .new_event()
479            .expect("Failed to allocate event");
480        let handle = event.into_handle();
481        let awaiter = self
482            .event_system
483            .awaiter(handle)
484            .expect("Failed to get awaiter");
485
486        let notification = notifications::RegisterNixlNotification {
487            uuid: Uuid::new_v4(),
488            xfer_req,
489            event_handle: handle,
490        };
491
492        // Send to background handler — log error if channel is full or closed
493        if let Err(e) = self.tx_nixl_events.try_send(notification) {
494            tracing::error!(
495                "Failed to enqueue NIXL event notification: channel full or closed: {}",
496                e
497            );
498        }
499
500        TransferCompleteNotification::from_awaiter(awaiter)
501    }
502
503    /// Get the worker ID for this context.
504    pub(crate) fn worker_id(&self) -> u64 {
505        self.worker_id
506    }
507}