1use std::sync::atomic::{AtomicU64, Ordering};
29use std::sync::Arc;
30use std::sync::Mutex;
31use std::sync::OnceLock;
32
33use cudarc::driver::{CudaEvent, CudaStream};
34use xlog_core::{Result, XlogError};
35
36use super::direct::DirectCudaResource;
37use super::resource::{
38 Access, AllocTag, BlockId, DeviceBlock, DeviceMemoryResource, ResourceError, ResourceResult,
39 StreamId,
40};
41use super::stream_pool::StreamPool;
42use crate::CudaDevice;
43
44pub const MAX_DEVICE_ORDINALS: usize = 16;
48
49static RUNTIMES: [OnceLock<&'static XlogDeviceRuntime>; MAX_DEVICE_ORDINALS] =
53 [const { OnceLock::new() }; MAX_DEVICE_ORDINALS];
54
55static INIT_LOCKS: [Mutex<()>; MAX_DEVICE_ORDINALS] =
60 [const { Mutex::new(()) }; MAX_DEVICE_ORDINALS];
61
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
64pub struct ConditionalGraphStats {
65 pub launches: u64,
67 pub terminal_synchronizations: u64,
69 pub host_iterations: u64,
71 pub host_allocations: u64,
73 pub device_status_writer_launches: u64,
75 pub host_status_injections: u64,
77}
78
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub struct EventLifecycleStats {
82 pub live_events: u64,
84 pub created_events: u64,
86 pub destroyed_events: u64,
88 pub drop_waits: u64,
90}
91
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94pub struct ResidentGraphHandleLifecycleStats {
95 pub live_graphs: u64,
97 pub live_graph_execs: u64,
99 pub created_graphs: u64,
101 pub destroyed_graphs: u64,
103 pub created_graph_execs: u64,
105 pub destroyed_graph_execs: u64,
107}
108
109#[derive(Default)]
110struct ResidentRuntimeTelemetry {
111 launches: AtomicU64,
112 terminal_synchronizations: AtomicU64,
113 host_iterations: AtomicU64,
114 host_allocations: AtomicU64,
115 device_status_writer_launches: AtomicU64,
116 host_status_injections: AtomicU64,
117 live_events: AtomicU64,
118 created_events: AtomicU64,
119 destroyed_events: AtomicU64,
120 drop_waits: AtomicU64,
121 live_graphs: AtomicU64,
122 live_graph_execs: AtomicU64,
123 created_graphs: AtomicU64,
124 destroyed_graphs: AtomicU64,
125 created_graph_execs: AtomicU64,
126 destroyed_graph_execs: AtomicU64,
127}
128
129pub(crate) struct ResidentGraphHandleLease {
135 telemetry: Arc<ResidentRuntimeTelemetry>,
136}
137
138impl Drop for ResidentGraphHandleLease {
139 fn drop(&mut self) {
140 self.telemetry
141 .live_graph_execs
142 .fetch_sub(1, Ordering::AcqRel);
143 self.telemetry
144 .destroyed_graph_execs
145 .fetch_add(1, Ordering::Relaxed);
146 self.telemetry.live_graphs.fetch_sub(1, Ordering::AcqRel);
147 self.telemetry
148 .destroyed_graphs
149 .fetch_add(1, Ordering::Relaxed);
150 }
151}
152
153pub struct ResidentCompletionEvent {
155 event: Option<CudaEvent>,
156 telemetry: Arc<ResidentRuntimeTelemetry>,
157 synchronized: bool,
158}
159
160impl ResidentCompletionEvent {
161 pub fn synchronize(&mut self) -> Result<()> {
163 if self.synchronized {
164 return Ok(());
165 }
166 self.event
167 .as_ref()
168 .expect("resident completion event missing before drop")
169 .synchronize()
170 .map_err(|error| {
171 XlogError::Kernel(format!(
172 "resident conditional graph terminal event synchronization failed: {error}"
173 ))
174 })?;
175 self.synchronized = true;
176 self.telemetry
177 .terminal_synchronizations
178 .fetch_add(1, Ordering::Relaxed);
179 Ok(())
180 }
181}
182
183impl Drop for ResidentCompletionEvent {
184 fn drop(&mut self) {
185 if !self.synchronized {
186 self.telemetry.drop_waits.fetch_add(1, Ordering::Relaxed);
187 if let Some(event) = &self.event {
188 let _ = event.synchronize();
191 }
192 }
193 if self.event.take().is_some() {
194 self.telemetry.live_events.fetch_sub(1, Ordering::AcqRel);
195 self.telemetry
196 .destroyed_events
197 .fetch_add(1, Ordering::Relaxed);
198 }
199 }
200}
201
202pub struct XlogDeviceRuntime {
210 device_ordinal: u32,
211 device: Arc<CudaDevice>,
212 stream_pool: Arc<StreamPool>,
213 resource: Mutex<Box<dyn DeviceMemoryResource + Send + Sync>>,
214 reservation_bytes: Mutex<usize>,
218 resident_telemetry: Arc<ResidentRuntimeTelemetry>,
219}
220
221pub(crate) struct RuntimeMemoryReservation {
223 runtime: Arc<XlogDeviceRuntime>,
224 total_bytes: usize,
225 remaining_bytes: usize,
226}
227
228impl RuntimeMemoryReservation {
229 pub(crate) fn allocate(
230 &mut self,
231 bytes: usize,
232 stream: StreamId,
233 tag: AllocTag,
234 ) -> ResourceResult<DeviceBlock> {
235 if bytes > self.remaining_bytes {
236 return Err(ResourceError::OutOfBudget {
237 requested: bytes,
238 current: self.total_bytes - self.remaining_bytes,
239 remaining: self.remaining_bytes,
240 limit: self.total_bytes,
241 });
242 }
243
244 let resource = self
245 .runtime
246 .resource
247 .lock()
248 .expect("device-runtime resource poisoned");
249 let mut reserved = self
250 .runtime
251 .reservation_bytes
252 .lock()
253 .expect("device-runtime reservation accounting poisoned");
254 *reserved = reserved.checked_sub(bytes).ok_or_else(|| {
255 ResourceError::Driver("device-runtime reservation accounting underflow".to_string())
256 })?;
257 self.remaining_bytes -= bytes;
258
259 match resource.allocate(bytes, stream, tag) {
260 Ok(block) => Ok(block),
261 Err(error) => {
262 *reserved = reserved.checked_add(bytes).ok_or_else(|| {
263 ResourceError::Driver(
264 "device-runtime reservation rollback overflow".to_string(),
265 )
266 })?;
267 self.remaining_bytes =
268 self.remaining_bytes.checked_add(bytes).ok_or_else(|| {
269 ResourceError::Driver("device-runtime token rollback overflow".to_string())
270 })?;
271 Err(error)
272 }
273 }
274 }
275}
276
277impl Drop for RuntimeMemoryReservation {
278 fn drop(&mut self) {
279 let mut reserved = self
280 .runtime
281 .reservation_bytes
282 .lock()
283 .expect("device-runtime reservation accounting poisoned");
284 *reserved = reserved
285 .checked_sub(self.remaining_bytes)
286 .expect("device-runtime reservation accounting underflow");
287 self.remaining_bytes = 0;
288 }
289}
290
291impl XlogDeviceRuntime {
292 pub fn with_resource(
319 device: Arc<CudaDevice>,
320 device_ordinal: u32,
321 stream_pool: Arc<StreamPool>,
322 resource: Box<dyn DeviceMemoryResource + Send + Sync>,
323 ) -> Self {
324 Self {
325 device_ordinal,
326 device,
327 stream_pool,
328 resource: Mutex::new(resource),
329 reservation_bytes: Mutex::new(0),
330 resident_telemetry: Arc::new(ResidentRuntimeTelemetry::default()),
331 }
332 }
333
334 pub(crate) fn reserve_memory(
338 self: &Arc<Self>,
339 bytes: usize,
340 ) -> ResourceResult<RuntimeMemoryReservation> {
341 let resource = self
342 .resource
343 .lock()
344 .expect("device-runtime resource poisoned");
345 let snapshot = resource.budget_snapshot().ok_or_else(|| {
346 ResourceError::Driver(
347 "device-runtime resource stack has no reservable global budget".to_string(),
348 )
349 })?;
350 let mut reserved = self
351 .reservation_bytes
352 .lock()
353 .expect("device-runtime reservation accounting poisoned");
354 let current = snapshot.reserved.checked_add(*reserved).ok_or_else(|| {
355 ResourceError::Driver("device-runtime reservation accounting overflow".to_string())
356 })?;
357 let remaining = snapshot.limit.saturating_sub(current);
358 if bytes > remaining {
359 return Err(ResourceError::OutOfBudget {
360 requested: bytes,
361 current,
362 remaining,
363 limit: snapshot.limit,
364 });
365 }
366 *reserved = reserved.checked_add(bytes).ok_or_else(|| {
367 ResourceError::Driver("device-runtime reservation accounting overflow".to_string())
368 })?;
369 Ok(RuntimeMemoryReservation {
370 runtime: Arc::clone(self),
371 total_bytes: bytes,
372 remaining_bytes: bytes,
373 })
374 }
375
376 pub fn try_get(ordinal: u32) -> Result<&'static XlogDeviceRuntime> {
390 let idx = ordinal as usize;
391 if idx >= MAX_DEVICE_ORDINALS {
392 return Err(XlogError::Kernel(format!(
393 "XlogDeviceRuntime: ordinal {} exceeds MAX_DEVICE_ORDINALS={}",
394 ordinal, MAX_DEVICE_ORDINALS
395 )));
396 }
397 if let Some(rt) = RUNTIMES[idx].get() {
399 return Ok(*rt);
400 }
401
402 let _guard = INIT_LOCKS[idx]
406 .lock()
407 .expect("XlogDeviceRuntime init mutex poisoned");
408
409 if let Some(rt) = RUNTIMES[idx].get() {
412 return Ok(*rt);
413 }
414
415 let device = Arc::new(CudaDevice::new(ordinal as usize).map_err(|e| {
419 XlogError::Kernel(format!(
420 "XlogDeviceRuntime: failed to open device {}: {}",
421 ordinal, e
422 ))
423 })?);
424 let stream_pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
425 let resource: Box<dyn DeviceMemoryResource + Send + Sync> =
426 Box::new(DirectCudaResource::new(Arc::clone(&device), ordinal));
427 let runtime = Box::new(XlogDeviceRuntime {
428 device_ordinal: ordinal,
429 device,
430 stream_pool,
431 resource: Mutex::new(resource),
432 reservation_bytes: Mutex::new(0),
433 resident_telemetry: Arc::new(ResidentRuntimeTelemetry::default()),
434 });
435 let leaked: &'static XlogDeviceRuntime = Box::leak(runtime);
436
437 RUNTIMES[idx]
442 .set(leaked)
443 .map_err(|_| ())
444 .expect("XlogDeviceRuntime: OnceLock::set raced under INIT_LOCKS — bug");
445 Ok(leaked)
446 }
447
448 pub fn device_ordinal(&self) -> u32 {
450 self.device_ordinal
451 }
452
453 pub fn device(&self) -> &Arc<CudaDevice> {
455 &self.device
456 }
457
458 pub fn stream_pool(&self) -> &Arc<StreamPool> {
460 &self.stream_pool
461 }
462
463 pub fn conditional_graph_stats(&self) -> ConditionalGraphStats {
465 let telemetry = &self.resident_telemetry;
466 ConditionalGraphStats {
467 launches: telemetry.launches.load(Ordering::Relaxed),
468 terminal_synchronizations: telemetry.terminal_synchronizations.load(Ordering::Relaxed),
469 host_iterations: telemetry.host_iterations.load(Ordering::Relaxed),
470 host_allocations: telemetry.host_allocations.load(Ordering::Relaxed),
471 device_status_writer_launches: telemetry
472 .device_status_writer_launches
473 .load(Ordering::Relaxed),
474 host_status_injections: telemetry.host_status_injections.load(Ordering::Relaxed),
475 }
476 }
477
478 pub fn reset_conditional_graph_stats(&self) {
483 let telemetry = &self.resident_telemetry;
484 telemetry.launches.store(0, Ordering::Relaxed);
485 telemetry
486 .terminal_synchronizations
487 .store(0, Ordering::Relaxed);
488 telemetry.host_iterations.store(0, Ordering::Relaxed);
489 telemetry.host_allocations.store(0, Ordering::Relaxed);
490 telemetry
491 .device_status_writer_launches
492 .store(0, Ordering::Relaxed);
493 telemetry.host_status_injections.store(0, Ordering::Relaxed);
494 }
495
496 pub fn event_lifecycle_stats(&self) -> EventLifecycleStats {
498 let telemetry = &self.resident_telemetry;
499 EventLifecycleStats {
500 live_events: telemetry.live_events.load(Ordering::Acquire),
501 created_events: telemetry.created_events.load(Ordering::Relaxed),
502 destroyed_events: telemetry.destroyed_events.load(Ordering::Relaxed),
503 drop_waits: telemetry.drop_waits.load(Ordering::Relaxed),
504 }
505 }
506
507 pub fn resident_graph_handle_lifecycle_stats(&self) -> ResidentGraphHandleLifecycleStats {
509 let telemetry = &self.resident_telemetry;
510 ResidentGraphHandleLifecycleStats {
511 live_graphs: telemetry.live_graphs.load(Ordering::Acquire),
512 live_graph_execs: telemetry.live_graph_execs.load(Ordering::Acquire),
513 created_graphs: telemetry.created_graphs.load(Ordering::Relaxed),
514 destroyed_graphs: telemetry.destroyed_graphs.load(Ordering::Relaxed),
515 created_graph_execs: telemetry.created_graph_execs.load(Ordering::Relaxed),
516 destroyed_graph_execs: telemetry.destroyed_graph_execs.load(Ordering::Relaxed),
517 }
518 }
519
520 pub(crate) fn resident_graph_handle_lease(&self) -> ResidentGraphHandleLease {
522 let telemetry = Arc::clone(&self.resident_telemetry);
523 telemetry.live_graphs.fetch_add(1, Ordering::AcqRel);
524 telemetry.created_graphs.fetch_add(1, Ordering::Relaxed);
525 telemetry.live_graph_execs.fetch_add(1, Ordering::AcqRel);
526 telemetry
527 .created_graph_execs
528 .fetch_add(1, Ordering::Relaxed);
529 ResidentGraphHandleLease { telemetry }
530 }
531
532 #[doc(hidden)]
534 pub fn record_conditional_graph_launch(&self, has_device_status_writer: bool) {
535 self.resident_telemetry
536 .launches
537 .fetch_add(1, Ordering::Relaxed);
538 if has_device_status_writer {
539 self.resident_telemetry
540 .device_status_writer_launches
541 .fetch_add(1, Ordering::Relaxed);
542 }
543 }
544
545 #[doc(hidden)]
547 pub fn record_resident_completion_event(
548 &self,
549 stream: &CudaStream,
550 ) -> Result<ResidentCompletionEvent> {
551 let event = stream.record_event(None).map_err(|error| {
552 XlogError::Kernel(format!(
553 "resident conditional graph completion event record failed: {error}"
554 ))
555 })?;
556 let telemetry = Arc::clone(&self.resident_telemetry);
557 telemetry.live_events.fetch_add(1, Ordering::AcqRel);
558 telemetry.created_events.fetch_add(1, Ordering::Relaxed);
559 Ok(ResidentCompletionEvent {
560 event: Some(event),
561 telemetry,
562 synchronized: false,
563 })
564 }
565
566 pub fn allocate(
569 &self,
570 bytes: usize,
571 stream: StreamId,
572 tag: AllocTag,
573 ) -> ResourceResult<DeviceBlock> {
574 let resource = self
575 .resource
576 .lock()
577 .expect("device-runtime resource poisoned");
578 if let Some(snapshot) = resource.budget_snapshot() {
579 let reserved = self
580 .reservation_bytes
581 .lock()
582 .expect("device-runtime reservation accounting poisoned");
583 let current = snapshot.reserved.checked_add(*reserved).ok_or_else(|| {
584 ResourceError::Driver("device-runtime reservation accounting overflow".to_string())
585 })?;
586 let remaining = snapshot.limit.saturating_sub(current);
587 if bytes > remaining {
588 return Err(ResourceError::OutOfBudget {
589 requested: bytes,
590 current,
591 remaining,
592 limit: snapshot.limit,
593 });
594 }
595 }
596 resource.allocate(bytes, stream, tag)
597 }
598
599 pub fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
601 self.resource
602 .lock()
603 .expect("device-runtime resource poisoned")
604 .deallocate(block)
605 }
606
607 pub fn bytes_outstanding(&self) -> usize {
611 self.resource
612 .lock()
613 .expect("device-runtime resource poisoned")
614 .bytes_outstanding()
615 }
616
617 pub fn reap_pending(&self) -> ResourceResult<()> {
622 self.resource
623 .lock()
624 .expect("device-runtime resource poisoned")
625 .reap_pending()
626 }
627
628 pub fn record_block_use(
642 &self,
643 block: &DeviceBlock,
644 use_stream: StreamId,
645 ) -> ResourceResult<()> {
646 self.resource
647 .lock()
648 .expect("device-runtime resource poisoned")
649 .record_block_use(block, use_stream)
650 }
651
652 pub fn supports_block_use_tracking(&self) -> bool {
658 self.resource
659 .lock()
660 .expect("device-runtime resource poisoned")
661 .supports_block_use_tracking()
662 }
663
664 pub fn prepare_block_use(
671 &self,
672 block: BlockId,
673 use_stream: StreamId,
674 access: Access,
675 ) -> ResourceResult<()> {
676 self.resource
677 .lock()
678 .expect("device-runtime resource poisoned")
679 .prepare_block_use(block, use_stream, access)
680 }
681
682 pub fn finish_block_use(
689 &self,
690 block: BlockId,
691 use_stream: StreamId,
692 access: Access,
693 ) -> ResourceResult<()> {
694 self.resource
695 .lock()
696 .expect("device-runtime resource poisoned")
697 .finish_block_use(block, use_stream, access)
698 }
699
700 pub fn prepare_first_use<T: cudarc::driver::DeviceRepr>(
715 &self,
716 slice: &crate::memory::TrackedCudaSlice<T>,
717 use_stream: StreamId,
718 access: Access,
719 ) -> ResourceResult<()> {
720 let block = slice.runtime_block().ok_or_else(|| {
721 super::resource::ResourceError::StreamMisuse(
722 "prepare_first_use: slice is not runtime-backed (the helper's \
723 GpuMemoryManager must be built via with_runtime)"
724 .to_string(),
725 )
726 })?;
727 self.prepare_block_use(BlockId::from_block(block), use_stream, access)
728 }
729
730 pub fn finish_first_use<T: cudarc::driver::DeviceRepr>(
734 &self,
735 slice: &crate::memory::TrackedCudaSlice<T>,
736 use_stream: StreamId,
737 access: Access,
738 ) -> ResourceResult<()> {
739 let block = slice.runtime_block().ok_or_else(|| {
740 super::resource::ResourceError::StreamMisuse(
741 "finish_first_use: slice is not runtime-backed".to_string(),
742 )
743 })?;
744 self.finish_block_use(BlockId::from_block(block), use_stream, access)
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 fn try_runtime() -> Option<&'static XlogDeviceRuntime> {
753 match XlogDeviceRuntime::try_get(0) {
754 Ok(runtime) => Some(runtime),
755 Err(error) => {
756 if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
757 panic!(
758 "XLOG_REQUIRE_CUDA=1 but CUDA is unavailable \
759 (XlogDeviceRuntime::try_get): {error}"
760 );
761 }
762 eprintln!(
763 "Skipping device-runtime test: CUDA unavailable \
764 (XlogDeviceRuntime::try_get): {error}"
765 );
766 None
767 }
768 }
769 }
770
771 #[test]
772 fn try_get_returns_same_singleton() {
773 let Some(a) = try_runtime() else {
774 return;
775 };
776 let b = XlogDeviceRuntime::try_get(0).expect("re-get");
777 assert!(std::ptr::eq(a, b), "singleton must be stable for ordinal 0");
778 assert_eq!(a.device_ordinal(), 0);
779 }
780
781 #[test]
782 fn allocate_then_deallocate_via_runtime() {
783 let Some(rt) = try_runtime() else {
784 return;
785 };
786 let before = rt.bytes_outstanding();
787 let block = rt
788 .allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED)
789 .expect("alloc");
790 assert_eq!(block.bytes, 2048);
791 assert_eq!(rt.bytes_outstanding(), before + 2048);
792 rt.deallocate(block).expect("dealloc");
793 rt.reap_pending().expect("reap pending");
794 assert_eq!(rt.bytes_outstanding(), before);
795 }
796
797 #[test]
798 fn try_get_rejects_out_of_range_ordinal() {
799 let err = XlogDeviceRuntime::try_get(MAX_DEVICE_ORDINALS as u32);
800 assert!(err.is_err());
801 }
802
803 #[test]
804 fn with_resource_composes_owned_runtime_outside_singleton() {
805 use super::super::async_resource::AsyncCudaResource;
806
807 let Some(rt) = try_runtime() else {
808 return;
809 };
810 let device = Arc::clone(rt.device());
811 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
812 let resource = Box::new(AsyncCudaResource::new(
813 Arc::clone(&device),
814 0,
815 Arc::clone(&pool),
816 ));
817
818 let owned = XlogDeviceRuntime::with_resource(device, 0, pool, resource);
819 assert_eq!(owned.device_ordinal(), 0);
820
821 let block = owned
822 .allocate(1024, StreamId::DEFAULT, AllocTag::UNTAGGED)
823 .expect("alloc through composed runtime");
824 assert_eq!(block.bytes, 1024);
825 assert_eq!(owned.bytes_outstanding(), 1024);
826 owned.deallocate(block).expect("dealloc");
827 owned.reap_pending().expect("reap");
828 assert_eq!(owned.bytes_outstanding(), 0);
829
830 let singleton = XlogDeviceRuntime::try_get(0).expect("singleton");
834 assert!(
835 !std::ptr::eq(&owned, singleton),
836 "with_resource must not aliase the singleton slot"
837 );
838 }
839
840 #[test]
841 fn resident_completion_event_accounts_a_real_recorded_event() {
842 let Some(runtime) = try_runtime() else {
843 return;
844 };
845 let stream = runtime
846 .stream_pool()
847 .resolve(StreamId::DEFAULT)
848 .expect("default stream");
849 let before = runtime.event_lifecycle_stats();
850 let mut completion = runtime
851 .record_resident_completion_event(&stream)
852 .expect("record completion event");
853 let live = runtime.event_lifecycle_stats();
854 assert_eq!(live.live_events, before.live_events + 1);
855 assert_eq!(live.created_events, before.created_events + 1);
856 completion
857 .synchronize()
858 .expect("synchronize completion event");
859 drop(completion);
860 let after = runtime.event_lifecycle_stats();
861 assert_eq!(after.live_events, before.live_events);
862 assert_eq!(after.destroyed_events, before.destroyed_events + 1);
863 assert_eq!(after.drop_waits, before.drop_waits);
864 }
865
866 #[test]
867 fn resident_graph_handle_lease_balances_one_owner_slot() {
868 let Some(runtime) = try_runtime() else {
869 return;
870 };
871 let before = runtime.resident_graph_handle_lifecycle_stats();
872 let lease = runtime.resident_graph_handle_lease();
873 let live = runtime.resident_graph_handle_lifecycle_stats();
874 assert_eq!(live.live_graphs, before.live_graphs + 1);
875 assert_eq!(live.live_graph_execs, before.live_graph_execs + 1);
876 drop(lease);
877 let after = runtime.resident_graph_handle_lifecycle_stats();
878 assert_eq!(after.live_graphs, before.live_graphs);
879 assert_eq!(after.live_graph_execs, before.live_graph_execs);
880 assert_eq!(
881 after.created_graphs - before.created_graphs,
882 after.destroyed_graphs - before.destroyed_graphs
883 );
884 assert_eq!(
885 after.created_graph_execs - before.created_graph_execs,
886 after.destroyed_graph_execs - before.destroyed_graph_execs
887 );
888 }
889
890 #[test]
900 fn try_get_runtime_record_block_use_rejected_with_stream_misuse() {
901 let Some(rt) = try_runtime() else {
902 return;
903 };
904 let block = rt
905 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
906 .expect("alloc through runtime");
907 let err = rt.record_block_use(&block, StreamId::DEFAULT);
908 match err {
909 Err(super::super::resource::ResourceError::StreamMisuse(msg)) => {
910 assert!(
911 msg.contains("unsupported"),
912 "expected 'unsupported' in StreamMisuse message, got {:?}",
913 msg
914 );
915 }
916 other => panic!(
917 "XlogDeviceRuntime::try_get default (DirectCudaResource) must \
918 reject record_block_use with StreamMisuse; got {:?}",
919 other
920 ),
921 }
922 rt.deallocate(block).expect("dealloc still works");
923 }
924}