use crate::metadata_cache::CacheMode;
use crate::server::{BufferBinding, ServerError};
use alloc::format;
use alloc::vec::Vec;
use cubecl_common::bytes::Bytes;
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::stream::StreamId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum StreamCaptureState {
#[default]
NoCapture,
Prepare {
owner: StreamId,
},
Capture {
owner: StreamId,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureEnd {
Owned {
owner: StreamId,
},
Abandoned {
owner: StreamId,
},
}
impl CaptureEnd {
pub fn owner(&self) -> StreamId {
match self {
CaptureEnd::Owned { owner } | CaptureEnd::Abandoned { owner } => *owner,
}
}
pub fn is_abandoned(&self) -> bool {
matches!(self, CaptureEnd::Abandoned { .. })
}
pub fn abandoned_error(&self, caller: StreamId, doomed: Option<ServerError>) -> ServerError {
let mut errors = alloc::vec![ServerError::graph_state(format!(
"end_capture: the capture belongs to logical stream {:?}, not to {caller:?}; it is \
discarded rather than left recording on a stream both share",
self.owner(),
))];
errors.extend(doomed);
ServerError::Several {
errors,
backtrace: BackTrace::capture(),
}
}
}
#[derive(Debug, Default)]
pub struct StreamCapture {
state: StreamCaptureState,
recorded: Vec<BufferBinding>,
retained_host: Vec<Bytes>,
failed: Option<ServerError>,
}
impl StreamCapture {
pub fn record(&mut self, buffers: impl IntoIterator<Item = BufferBinding>) {
if self.state.is_recording() {
self.recorded.extend(buffers);
}
}
pub fn take_recorded(&mut self) -> Vec<BufferBinding> {
let mut recorded = core::mem::take(&mut self.recorded);
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 {
self.state.cache_mode()
}
pub fn prepare(&mut self, owner: StreamId) -> Result<(), ServerError> {
self.state.prepare(owner)?;
self.recorded.clear();
self.retained_host.clear();
self.failed = None;
Ok(())
}
pub fn begin(&mut self) -> Result<(), ServerError> {
self.state.begin()
}
pub fn end(&mut self, caller: StreamId) -> Result<CaptureEnd, ServerError> {
self.state.end(caller)
}
pub fn abort(&mut self) {
self.state.abort();
self.recorded.clear();
self.retained_host.clear();
self.failed = None;
}
}
impl StreamCaptureState {
pub(crate) fn is_recording(&self) -> bool {
matches!(self, StreamCaptureState::Capture { .. })
}
pub(crate) fn is_active(&self) -> bool {
!matches!(self, StreamCaptureState::NoCapture)
}
pub(crate) fn owner(&self) -> Option<StreamId> {
match self {
StreamCaptureState::NoCapture => None,
StreamCaptureState::Prepare { owner } | StreamCaptureState::Capture { owner } => {
Some(*owner)
}
}
}
pub(crate) fn cache_mode(&self) -> CacheMode {
match self {
StreamCaptureState::NoCapture => CacheMode::Normal,
StreamCaptureState::Prepare { .. } | StreamCaptureState::Capture { .. } => {
CacheMode::Capture
}
}
}
pub(crate) fn prepare(&mut self, owner: StreamId) -> Result<(), ServerError> {
match self {
StreamCaptureState::NoCapture => {
*self = StreamCaptureState::Prepare { owner };
Ok(())
}
StreamCaptureState::Prepare { .. } => Err(ServerError::graph_state(
"graph_prepare: a graph capture is already prepared on this stream",
)),
StreamCaptureState::Capture { .. } => Err(ServerError::graph_state(
"graph_prepare: a graph capture is already recording on this stream",
)),
}
}
pub(crate) fn begin(&mut self) -> Result<(), ServerError> {
match self {
StreamCaptureState::Prepare { owner } => {
*self = StreamCaptureState::Capture { owner: *owner };
Ok(())
}
StreamCaptureState::NoCapture => Err(ServerError::graph_state(
"begin_capture: call graph_prepare before starting a capture",
)),
StreamCaptureState::Capture { .. } => Err(ServerError::graph_state(
"begin_capture: a graph capture is already recording on this stream",
)),
}
}
pub(crate) fn end(&mut self, caller: StreamId) -> Result<CaptureEnd, ServerError> {
match self {
StreamCaptureState::Capture { owner } => {
let owner = *owner;
*self = StreamCaptureState::NoCapture;
Ok(match owner == caller {
true => CaptureEnd::Owned { owner },
false => CaptureEnd::Abandoned { owner },
})
}
StreamCaptureState::NoCapture | StreamCaptureState::Prepare { .. } => {
Err(ServerError::graph_state(
"end_capture: no graph capture is recording on this stream",
))
}
}
}
pub(crate) fn abort(&mut self) {
*self = StreamCaptureState::NoCapture;
}
}
#[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 {
Handle::new(service(), OWNER, 8).binding()
}
fn ids(bindings: &[BufferBinding]) -> Vec<ManagedMemoryId> {
bindings.iter().map(|binding| binding.memory.id()).collect()
}
#[test]
fn transitions_follow_the_capture_order() {
let mut state = StreamCaptureState::NoCapture;
assert!(state.begin().is_err(), "a capture must be prepared first");
assert!(state.end(OWNER).is_err(), "nothing is recording yet");
assert_eq!(state, StreamCaptureState::NoCapture);
state.prepare(OWNER).unwrap();
assert_eq!(state, StreamCaptureState::Prepare { owner: OWNER });
assert!(state.prepare(OWNER).is_err(), "one prepare per capture");
assert!(state.end(OWNER).is_err(), "the window never opened");
state.begin().unwrap();
assert_eq!(state, StreamCaptureState::Capture { owner: OWNER });
assert!(state.begin().is_err(), "captures may not overlap");
assert!(state.prepare(OWNER).is_err(), "captures may not overlap");
assert_eq!(
state.end(OWNER).unwrap(),
CaptureEnd::Owned { owner: OWNER }
);
assert_eq!(state, StreamCaptureState::NoCapture);
}
#[test]
fn the_window_carries_its_owner() {
let mut state = StreamCaptureState::NoCapture;
assert_eq!(state.owner(), None);
state.prepare(OWNER).unwrap();
assert_eq!(state.owner(), Some(OWNER));
assert!(state.is_active(), "the window is open from prepare on");
state.begin().unwrap();
assert_eq!(state.owner(), Some(OWNER));
assert_eq!(state.end(OWNER).unwrap().owner(), OWNER);
assert_eq!(state.owner(), None);
assert!(!state.is_active());
}
#[test]
fn only_the_stream_that_opened_a_capture_may_seal_it() {
let neighbour = StreamId { value: 8 };
let mut state = StreamCaptureState::NoCapture;
state.prepare(OWNER).unwrap();
state.begin().unwrap();
assert_eq!(
state.end(neighbour).unwrap(),
CaptureEnd::Abandoned { owner: OWNER },
"the window is not theirs to seal"
);
}
#[test]
fn a_capture_no_one_can_close_does_not_wedge_the_stream() {
let neighbour = StreamId { value: 8 };
let mut state = StreamCaptureState::NoCapture;
state.prepare(OWNER).unwrap();
state.begin().unwrap();
assert!(state.end(neighbour).unwrap().is_abandoned());
assert_eq!(state, StreamCaptureState::NoCapture);
assert!(!state.is_active(), "the slot serves other work again");
state
.prepare(neighbour)
.expect("the stream is re-capturable");
state.begin().unwrap();
assert!(
state.end(OWNER).unwrap().is_abandoned(),
"the window it opened is long gone"
);
}
#[test]
fn a_capture_names_each_buffer_its_launches_were_given_once() {
let (a, b, c) = (buffer(), buffer(), buffer());
let mut capture = StreamCapture::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::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::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::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::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 an_abandoned_window_reports_whose_it_was_and_what_doomed_it() {
let caller = StreamId { value: 8 };
let outcome = CaptureEnd::Abandoned { owner: OWNER };
let error = outcome.abandoned_error(caller, Some(ServerError::graph_state("doomed")));
let ServerError::Several { errors, .. } = &error else {
panic!("an abandoned window reports several failures at once, got: {error:?}");
};
let reported = alloc::format!("{error}");
assert!(
reported.contains(&alloc::format!("{OWNER:?}"))
&& reported.contains(&alloc::format!("{caller:?}")),
"the report has to name the window's owner and the caller refused it, got: {reported}"
);
assert_eq!(errors.len(), 2, "the doomed reason travels with it");
assert!(
alloc::format!("{}", errors[1]).contains("doomed"),
"the explanation comes first, then what had already sunk it"
);
}
#[test]
fn a_rejected_transition_changes_nothing() {
let mut state = StreamCaptureState::Prepare { owner: OWNER };
assert!(state.prepare(OWNER).is_err());
assert_eq!(state, StreamCaptureState::Prepare { owner: OWNER });
state.begin().unwrap();
}
#[test]
fn abort_recovers_a_window_that_never_opened() {
for state in [
StreamCaptureState::Prepare { owner: OWNER },
StreamCaptureState::Capture { owner: OWNER },
] {
let mut state = state;
state.abort();
assert_eq!(state, StreamCaptureState::NoCapture);
state.prepare(OWNER).expect("the stream is re-capturable");
}
}
#[test]
fn the_cache_captures_across_the_whole_window() {
assert_eq!(
StreamCaptureState::NoCapture.cache_mode(),
CacheMode::Normal
);
assert_eq!(
StreamCaptureState::Prepare { owner: OWNER }.cache_mode(),
CacheMode::Capture
);
assert_eq!(
StreamCaptureState::Capture { owner: OWNER }.cache_mode(),
CacheMode::Capture
);
}
}