use crate::memory_management::{ManagedMemoryBinding, MemoryLocation, PageUpdate};
use crate::metadata_cache::CacheMode;
use crate::server::{BufferBinding, ServerError, WeakBufferBinding};
use alloc::vec::Vec;
use cubecl_common::bytes::Bytes;
use cubecl_environment::stream::StreamId;
use cubecl_runtime::server::{CaptureStatus, DeviceCaptures, StreamCaptureState};
pub use cubecl_runtime::server::CaptureEnd;
#[derive(Debug)]
pub struct StreamCapture {
state: StreamCaptureState,
recorded: Vec<WeakBufferBinding>,
touched: Vec<TouchedPage>,
primed: Vec<ManagedMemoryBinding>,
retained_host: Vec<Bytes>,
failed: Option<ServerError>,
}
impl StreamCapture {
pub fn new(device: &DeviceCaptures) -> Self {
Self {
state: device.stream(),
recorded: Vec::new(),
touched: Vec::new(),
primed: Vec::new(),
retained_host: Vec::new(),
failed: None,
}
}
pub fn device(&self) -> &CaptureStatus {
self.state.device()
}
pub fn record(&mut self, buffers: impl IntoIterator<Item = BufferBinding>) {
if self.state.is_recording() {
self.recorded
.extend(buffers.into_iter().map(|binding| binding.downgrade()));
}
}
pub fn touch(&mut self, stream: StreamId, location: MemoryLocation) {
if self.state.is_recording() {
self.touched.push(TouchedPage { stream, location });
}
}
pub fn take_touched(&mut self) -> Vec<TouchedPage> {
let mut touched = core::mem::take(&mut self.touched);
touched.sort_unstable();
touched.dedup();
touched
}
pub fn page_update(&self) -> PageUpdate {
match (self.is_recording(), self.device().any_recording()) {
(true, _) => PageUpdate::Forbidden,
(false, true) => PageUpdate::AddOnly,
(false, false) => PageUpdate::Allow,
}
}
pub fn prime(&mut self, staging: &ManagedMemoryBinding) {
if self.state.is_preparing() {
self.primed.push(staging.clone());
}
}
pub fn take_recorded(&mut self) -> Vec<BufferBinding> {
let mut recorded: Vec<BufferBinding> = core::mem::take(&mut self.recorded)
.iter()
.filter_map(WeakBufferBinding::upgrade)
.collect();
recorded.sort_unstable_by_key(|binding| binding.claim_key());
recorded.dedup_by_key(|binding| binding.claim_key());
recorded
}
pub fn fail(&mut self, error: ServerError) {
if self.state.is_recording() && self.failed.is_none() {
self.failed = Some(error);
}
}
pub fn retain_host(&mut self, bytes: Bytes) {
debug_assert!(
self.state.is_recording(),
"host bytes are the window's to retain only while it records"
);
self.retained_host.push(bytes);
}
pub fn take_retained_host(&mut self) -> Vec<Bytes> {
core::mem::take(&mut self.retained_host)
}
pub fn take_failure(&mut self) -> Option<ServerError> {
self.failed.take()
}
pub fn is_recording(&self) -> bool {
self.state.is_recording()
}
pub fn is_active(&self) -> bool {
self.state.is_active()
}
pub fn owner(&self) -> Option<StreamId> {
self.state.owner()
}
pub fn cache_mode(&self) -> CacheMode {
match self.state.is_active() {
true => CacheMode::Capture,
false => CacheMode::Normal,
}
}
pub fn prepare(&mut self, owner: StreamId) -> Result<(), ServerError> {
self.state.prepare(owner)?;
self.clear();
Ok(())
}
pub fn begin(&mut self) -> Result<(), ServerError> {
self.state.begin()?;
self.primed.clear();
Ok(())
}
pub fn end(&mut self, caller: StreamId) -> Result<CaptureEnd, ServerError> {
self.state.end(caller)
}
pub fn abort(&mut self) {
self.state.abort();
self.clear();
}
fn clear(&mut self) {
self.recorded.clear();
self.touched.clear();
self.primed.clear();
self.retained_host.clear();
self.failed = None;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct TouchedPage {
pub stream: StreamId,
pub location: MemoryLocation,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_management::ManagedMemoryId;
use crate::server::Handle;
fn service() -> cubecl_common::device::ServiceId {
cubecl_common::device::ServiceId::of::<()>(cubecl_common::device::DeviceId::new(0, 0))
}
const OWNER: StreamId = StreamId { value: 7 };
fn buffer() -> BufferBinding {
let binding = Handle::new(service(), OWNER, 8).binding();
core::mem::forget(binding.memory.clone());
binding
}
fn ids(bindings: &[BufferBinding]) -> Vec<ManagedMemoryId> {
bindings.iter().map(|binding| binding.memory.id()).collect()
}
#[test]
fn a_capture_names_each_buffer_its_launches_were_given_once() {
let (a, b, c) = (buffer(), buffer(), buffer());
let mut capture = StreamCapture::new(&DeviceCaptures::default());
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.record([a.clone(), b.clone()]);
capture.record([b.clone(), c.clone()]);
capture.record([a.clone()]);
assert_eq!(
capture.end(OWNER).unwrap(),
CaptureEnd::Owned { owner: OWNER }
);
assert_eq!(ids(&capture.take_recorded()), ids(&[a, b, c]));
assert!(
capture.take_recorded().is_empty(),
"the recording moves onto the graph, it is not left on the stream"
);
}
#[test]
fn a_launch_outside_a_window_is_not_recorded() {
let (before, warmup, recorded) = (buffer(), buffer(), buffer());
let mut capture = StreamCapture::new(&DeviceCaptures::default());
capture.record([before]);
capture.prepare(OWNER).unwrap();
capture.record([warmup]);
capture.begin().unwrap();
capture.record([recorded.clone()]);
capture.end(OWNER).unwrap();
assert_eq!(ids(&capture.take_recorded()), ids(&[recorded]));
}
#[test]
fn a_capture_names_every_range_of_a_batched_allocation() {
let handle = Handle::new(service(), OWNER, 8);
let mut front = handle.clone().binding();
front.offset_end = Some(4);
let mut back = handle.clone().binding();
back.offset_start = Some(4);
assert_eq!(
front.memory.id(),
back.memory.id(),
"one allocation carved in two is the case under test"
);
let mut capture = StreamCapture::new(&DeviceCaptures::default());
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.record([front.clone(), back.clone()]);
capture.record([front.clone()]);
capture.end(OWNER).unwrap();
let recorded = capture.take_recorded();
let keys: Vec<_> = recorded.iter().map(|binding| binding.claim_key()).collect();
assert_eq!(
keys,
alloc::vec![front.claim_key(), back.claim_key()],
"both siblings survive, each named once"
);
}
#[test]
fn a_window_owns_the_host_bytes_its_copies_read() {
let mut capture = StreamCapture::new(&DeviceCaptures::default());
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.retain_host(Bytes::from_bytes_vec(alloc::vec![7u8; 4]));
capture.end(OWNER).unwrap();
assert_eq!(capture.take_retained_host().len(), 1);
assert!(
capture.take_retained_host().is_empty(),
"taken means moved onto the graph, not copied"
);
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.retain_host(Bytes::from_bytes_vec(alloc::vec![7u8; 4]));
capture.abort();
capture.prepare(OWNER).unwrap();
assert!(
capture.take_retained_host().is_empty(),
"an aborted window keeps nothing alive"
);
}
#[test]
fn a_new_capture_starts_from_an_empty_recording() {
let neighbour = StreamId { value: 8 };
let (aborted, abandoned) = (buffer(), buffer());
let mut capture = StreamCapture::new(&DeviceCaptures::default());
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.record([aborted]);
capture.abort();
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.record([abandoned.clone()]);
assert!(capture.end(neighbour).unwrap().is_abandoned());
assert_eq!(ids(&capture.take_recorded()), ids(&[abandoned]));
capture.prepare(neighbour).unwrap();
capture.begin().unwrap();
assert!(
capture.take_recorded().is_empty(),
"the abandoned window's buffers are not this capture's to answer for"
);
}
#[test]
fn the_cache_captures_across_the_whole_window() {
let mut capture = StreamCapture::new(&DeviceCaptures::default());
assert_eq!(capture.cache_mode(), CacheMode::Normal);
capture.prepare(OWNER).unwrap();
assert_eq!(capture.cache_mode(), CacheMode::Capture);
capture.begin().unwrap();
assert_eq!(capture.cache_mode(), CacheMode::Capture);
capture.end(OWNER).unwrap();
assert_eq!(capture.cache_mode(), CacheMode::Normal);
}
#[test]
fn a_capture_is_seen_by_its_stream_until_it_ends() {
const OTHER: StreamId = StreamId { value: 8 };
let device = DeviceCaptures::default();
let status = device.status();
let mut capture = StreamCapture::new(&device);
assert!(!status.is_capturing(OWNER), "no capture before prepare");
capture.prepare(OWNER).unwrap();
assert!(status.is_capturing(OWNER), "the warmup run is part of it");
assert!(
!status.is_capturing(OTHER),
"another stream isn't capturing"
);
capture.begin().unwrap();
assert!(status.is_capturing(OWNER), "the recorded run is part of it");
capture.end(OWNER).unwrap();
assert!(!status.is_capturing(OWNER), "a sealed capture is over");
capture.prepare(OWNER).unwrap();
capture.begin().unwrap();
capture.end(OTHER).unwrap();
assert!(
!status.is_capturing(OWNER),
"an abandoned capture is over for its owner too"
);
capture.prepare(OWNER).unwrap();
capture.abort();
assert!(!status.is_capturing(OWNER), "an aborted capture is over");
let mut other = StreamCapture::new(&device);
other.prepare(OTHER).unwrap();
capture.prepare(OWNER).unwrap();
drop(capture);
assert!(
!status.is_capturing(OWNER),
"a dropped stream's capture is over"
);
assert!(
status.is_capturing(OTHER),
"another stream's capture is untouched"
);
}
#[test]
fn a_recording_is_seen_device_wide() {
let device = DeviceCaptures::default();
let mut recording = StreamCapture::new(&device);
let neighbour = StreamCapture::new(&device);
let status = device.status();
let recording_now =
|capture: &StreamCapture| (capture.page_update(), status.any_recording());
recording.prepare(OWNER).unwrap();
assert!(!status.any_recording(), "a warmup records nothing");
recording.begin().unwrap();
assert_eq!(recording_now(&recording), (PageUpdate::Forbidden, true));
assert_eq!(recording_now(&neighbour), (PageUpdate::AddOnly, true));
recording.end(OWNER).unwrap();
assert_eq!(recording_now(&neighbour), (PageUpdate::Allow, false));
recording.prepare(OWNER).unwrap();
recording.begin().unwrap();
recording.abort();
assert!(
!status.any_recording(),
"an aborted window leaves the count"
);
recording.prepare(OWNER).unwrap();
recording.abort();
assert!(
!status.any_recording(),
"aborting a warmup leaves the count alone"
);
recording.prepare(OWNER).unwrap();
recording.begin().unwrap();
drop(recording);
assert!(
!status.any_recording(),
"a stream dropped while recording leaves the count"
);
}
}