use crate::{self as cubecl};
use alloc::string::{String, ToString};
use cubecl::prelude::*;
use cubecl_common::bytes::Bytes;
use cubecl_environment::stream::StreamId;
use cubecl_runtime::config::{CubeClRuntimeConfig, RuntimeConfig};
use cubecl_runtime::runtime::Runtime;
use cubecl_runtime::server::Handle;
#[cube(launch)]
pub fn fill(out: &mut [u32], #[comptime] value: u32) {
if ABSOLUTE_POS < out.len() {
out[ABSOLUTE_POS] = value;
}
}
#[cube(launch_unchecked)]
pub fn rejected(out: &mut [u32], #[comptime] reason: String) {
push_validation_error(reason);
out[0] = 1u32;
}
#[cube(launch)]
pub fn copy(input: &[u32], out: &mut [u32]) {
if ABSOLUTE_POS < out.len() {
out[ABSOLUTE_POS] = input[ABSOLUTE_POS];
}
}
#[cube(launch_unchecked)]
pub fn rejected_with_input(input: &[u32], out: &mut [u32], #[comptime] reason: String) {
push_validation_error(reason);
out[0] = input[0];
}
fn sharing_one_pooled_stream(seed: u64) -> (StreamId, StreamId) {
let max_streams = CubeClRuntimeConfig::get().streaming.max_streams as u64;
(
StreamId { value: seed },
StreamId {
value: seed + max_streams,
},
)
}
fn launch_rejected_into(client: &Client, out: Handle, reason: &str) {
unsafe {
rejected::launch_unchecked(
&client.clone(),
CubeCount::new_single(),
CubeDim::new_1d(1),
BufferArg::from_raw_parts(out, 1),
reason.to_string(),
)
};
}
fn launch_rejected(client: &Client, reason: &str) -> Handle {
let out = client.empty(core::mem::size_of::<u32>());
launch_rejected_into(client, out.clone(), reason);
out
}
fn assert_rejected(client: &Client, out: Handle, reason: &str) {
let err = client
.read_one(out)
.expect_err("the kernel pushed a validation error, the launch must fail")
.to_string();
assert!(
err.contains(reason),
"the read must report the launch that never wrote the buffer, got: {err}"
);
}
pub fn test_two_workflows_on_one_stream_do_not_contaminate_each_other<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_013);
let size = core::mem::size_of::<u32>();
let launch_copy = |input: &Handle, out: &Handle| {
copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_1d(1),
unsafe { BufferArg::from_raw_parts(input.clone(), 1) },
unsafe { BufferArg::from_raw_parts(out.clone(), 1) },
);
};
let (doomed_end, healthy_end) = producer.executes(|| {
let doomed_head = launch_rejected(&client, "the doomed workflow");
let doomed_mid = client.empty(size);
let doomed_end = client.empty(size);
let healthy_head = client.empty(size);
fill::launch(
&client,
CubeCount::new_single(),
CubeDim::new_1d(1),
unsafe { BufferArg::from_raw_parts(healthy_head.clone(), 1) },
7,
);
let healthy_mid = client.empty(size);
let healthy_end = client.empty(size);
launch_copy(&doomed_head, &doomed_mid);
launch_copy(&healthy_head, &healthy_mid);
launch_copy(&doomed_mid, &doomed_end);
launch_copy(&healthy_mid, &healthy_end);
(doomed_end, healthy_end)
});
reader.executes(|| {
let healthy = client
.read_one(healthy_end)
.expect("a chain that shares no buffer with the failure is untouched");
assert_eq!(
u32::from_bytes(&healthy),
&[7],
"the healthy workflow's value has to survive its neighbour failing"
);
assert_rejected(&client, doomed_end, "the doomed workflow");
});
}
pub fn test_a_read_surfaces_the_producers_rejection<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_002);
let out = producer.executes(|| launch_rejected(&client, "producer"));
reader.executes(|| assert_rejected(&client, out.clone(), "producer"));
producer.executes(|| {
client
.flush()
.expect("a launch failure is not the flush's to report");
assert_rejected(&client, out, "producer");
});
}
pub fn test_a_read_surfaces_a_rejection_on_a_buffer_it_allocated_itself<R: Runtime>(
client: Client,
) {
let (producer, reader) = sharing_one_pooled_stream(1_000_003);
let out = reader.executes(|| client.empty(core::mem::size_of::<u32>()));
producer.executes(|| launch_rejected_into(&client, out.clone(), "foreign-write"));
reader.executes(|| assert_rejected(&client, out, "foreign-write"));
}
pub fn test_a_buffer_the_failure_never_touched_reads_normally<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_004);
let untouched = reader.executes(|| client.create_from_slice(u32::as_bytes(&[7u32])));
producer.executes(|| launch_rejected(&client, "unrelated"));
let read = reader.executes(|| {
client
.read_one(untouched)
.expect("nothing wrote to this one")
});
assert_eq!(u32::from_bytes(&read), &[7]);
}
pub fn test_a_write_lands_after_the_work_queued_on_its_buffers_stream<R: Runtime>(client: Client) {
let owner = StreamId { value: 2_000_001 };
let writer = StreamId { value: 2_000_002 };
let len = 4;
let buf = owner.executes(|| {
let buf = client.empty(len * core::mem::size_of::<u32>());
fill::launch(
&client,
CubeCount::new_single(),
CubeDim::new_1d(len as u32),
unsafe { BufferArg::from_raw_parts(buf.clone(), len) },
1,
);
buf
});
writer.executes(|| {
client.write(
&buf,
Bytes::from_bytes_vec(u32::as_bytes(&[9u32; 4]).to_vec()),
)
});
let read = writer.executes(|| client.read_one(buf).expect("both writes succeeded"));
assert_eq!(
u32::from_bytes(&read),
&[9, 9, 9, 9],
"the host write was issued last, so it has to land last"
);
}
pub fn test_a_launch_that_never_compiled_taints_only_its_outputs<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_005);
let (input, out) = producer.executes(|| {
let input = client.create_from_slice(u32::as_bytes(&[7u32]));
let out = client.empty(core::mem::size_of::<u32>());
unsafe {
rejected_with_input::launch_unchecked(
&client.clone(),
CubeCount::new_single(),
CubeDim::new_1d(1),
BufferArg::from_raw_parts(input.clone(), 1),
BufferArg::from_raw_parts(out.clone(), 1),
"read-only-input".to_string(),
)
};
(input, out)
});
reader.executes(|| assert_rejected(&client, out.clone(), "read-only-input"));
let read = reader.executes(|| {
client
.read_one(input)
.expect("a launch that never compiled leaves its inputs readable")
});
assert_eq!(u32::from_bytes(&read), &[7]);
producer
.executes(|| client.write(&out, Bytes::from_bytes_vec(u32::as_bytes(&[9u32]).to_vec())));
let read = reader.executes(|| {
client
.read_one(out)
.expect("a rewritten buffer reads again")
});
assert_eq!(u32::from_bytes(&read), &[9]);
}
pub fn test_a_buffer_stays_unreadable_after_the_failure_was_reported<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_007);
let out = producer.executes(|| launch_rejected(&client, "reported-once"));
producer.executes(|| {
client
.flush()
.expect("a launch failure is not the flush's to report");
client.flush().expect("on any flush, however many");
});
reader.executes(|| assert_rejected(&client, out.clone(), "reported-once"));
producer.executes(|| assert_rejected(&client, out, "reported-once"));
}
pub fn test_a_host_write_makes_a_stale_buffer_readable_again<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_008);
let out = producer.executes(|| {
let out = client.empty(core::mem::size_of::<u32>());
launch_rejected_into(&client, out.clone(), "recovered");
out
});
reader.executes(|| assert_rejected(&client, out.clone(), "recovered"));
producer.executes(|| {
client.write(
&out,
Bytes::from_bytes_vec(u32::as_bytes(&[42u32]).to_vec()),
)
});
let read = reader.executes(|| {
client
.read_one(out)
.expect("the host write filled what the launch never did")
});
assert_eq!(u32::from_bytes(&read), &[42]);
producer.executes(|| {
client
.flush()
.expect("a launch failure is not the flush's to report")
});
}
pub fn test_a_relaunch_makes_a_stale_buffer_readable_again<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_009);
let len = 4;
let out = producer.executes(|| {
let out = client.empty(len * core::mem::size_of::<u32>());
launch_rejected_into(&client, out.clone(), "relaunched");
fill::launch(
&client,
CubeCount::new_single(),
CubeDim::new_1d(len as u32),
unsafe { BufferArg::from_raw_parts(out.clone(), len) },
3,
);
out
});
let read = reader.executes(|| {
client
.read_one(out)
.expect("the second launch wrote what the first never did")
});
assert_eq!(u32::from_bytes(&read), &[3; 4]);
}
pub fn test_a_read_downstream_of_a_failure_fails_on_the_root_cause<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_011);
let launch_copy = |input: &Handle, out: &Handle| {
copy::launch(
&client,
CubeCount::new_single(),
CubeDim::new_1d(1),
unsafe { BufferArg::from_raw_parts(input.clone(), 1) },
unsafe { BufferArg::from_raw_parts(out.clone(), 1) },
);
};
let (out1, out2, out3) = producer.executes(|| {
let out1 = launch_rejected(&client, "root-cause");
let out2 = client.empty(core::mem::size_of::<u32>());
let out3 = client.empty(core::mem::size_of::<u32>());
launch_copy(&out1, &out2);
launch_copy(&out2, &out3);
(out1, out2, out3)
});
let err = reader.executes(|| {
client
.read_one(out3)
.expect_err("the copies never ran, so nothing wrote this buffer")
.to_string()
});
assert!(
err.contains("root-cause"),
"the root failure travels the whole chain: {err}"
);
assert!(
err.matches("skipped `").count() == 2,
"both skipped hops are named on the way to the root: {err}"
);
assert!(
err.contains("failure #"),
"the failure id ties every read of this failure together: {err}"
);
reader.executes(|| assert_rejected(&client, out2, "root-cause"));
reader.executes(|| assert_rejected(&client, out1, "root-cause"));
}
pub fn test_check_answers_without_a_read<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_013);
let (stale, clean) = producer.executes(|| {
let stale = launch_rejected(&client, "checked-not-read");
let clean = client.create_from_slice(u32::as_bytes(&[5u32]));
(stale, clean)
});
reader.executes(|| {
client
.check([&clean])
.expect("a buffer whose writer succeeded checks clean");
let err = client
.check([&stale])
.expect_err("the rejected launch never wrote this one")
.to_string();
assert!(
err.contains("checked-not-read"),
"the check names the failure a read would have named: {err}"
);
});
reader.executes(|| assert_rejected(&client, stale, "checked-not-read"));
}
pub fn test_a_tainted_dynamic_cube_count_skips_the_launch<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_015);
let (count, out) = producer.executes(|| {
let count = client.empty(3 * core::mem::size_of::<u32>());
launch_rejected_into(&client, count.clone(), "tainted-count");
let out = client.empty(core::mem::size_of::<u32>());
fill::launch(
&client,
CubeCount::Dynamic(count.clone().binding()),
CubeDim::new_1d(1),
unsafe { BufferArg::from_raw_parts(out.clone(), 1) },
9,
);
(count, out)
});
reader.executes(|| assert_rejected(&client, out, "tainted-count"));
reader.executes(|| assert_rejected(&client, count, "tainted-count"));
}
pub fn test_a_zero_cube_count_launch_does_not_untaint_its_outputs<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_017);
let (out, count) = producer.executes(|| {
let out = client.empty(core::mem::size_of::<u32>());
launch_rejected_into(&client, out.clone(), "zero-count-survivor");
let count = client.create_from_slice(u32::as_bytes(&[0, 0, 0]));
fill::launch(
&client,
CubeCount::Dynamic(count.clone().binding()),
CubeDim::new_1d(1),
unsafe { BufferArg::from_raw_parts(out.clone(), 1) },
9,
);
(out, count)
});
reader.executes(|| assert_rejected(&client, out, "zero-count-survivor"));
let read = reader.executes(|| client.read_one(count).expect("the count was written"));
assert_eq!(u32::from_bytes(&read), &[0, 0, 0]);
}
pub fn test_a_failed_in_place_launch_names_the_buffer_it_aliased<R: Runtime>(client: Client) {
let (producer, reader) = sharing_one_pooled_stream(1_000_006);
let inout = producer.executes(|| {
let inout = client.create_from_slice(u32::as_bytes(&[7u32]));
unsafe {
rejected_with_input::launch_unchecked(
&client.clone(),
CubeCount::new_single(),
CubeDim::new_1d(1),
BufferArg::from_raw_parts(inout.clone(), 1),
BufferArg::alias(0, 1),
"aliased-output".to_string(),
)
};
inout
});
reader.executes(|| assert_rejected(&client, inout, "aliased-output"));
}
#[cfg(not(target_family = "wasm"))]
pub fn lazy_read_of_stale_buffer_fails_at_read<R: Runtime>(client: Client) {
let out = launch_rejected(&client, "lazy-read");
let descriptor = out.copy_descriptor(
cubecl_zspace::shape![1],
cubecl_zspace::strides![1],
core::mem::size_of::<u32>(),
);
let err = cubecl_environment::future::block_on(client.read_lazy_async(descriptor))
.expect_err("the buffer's writer was refused, the lazy read must say so")
.to_string();
assert!(
err.contains("lazy-read"),
"the lazy read must report the launch that never wrote the buffer, got: {err}"
);
}
#[allow(missing_docs)]
#[macro_export]
macro_rules! testgen_stream_errors {
() => {
mod stream_errors {
use super::*;
#[cfg(not(target_family = "wasm"))]
#[$crate::runtime_tests::test_log::test]
fn lazy_read_of_stale_buffer_fails_at_read() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::lazy_read_of_stale_buffer_fails_at_read::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_read_surfaces_the_producers_rejection() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_read_surfaces_the_producers_rejection::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_read_surfaces_a_rejection_on_a_buffer_it_allocated_itself() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_read_surfaces_a_rejection_on_a_buffer_it_allocated_itself::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_launch_that_never_compiled_taints_only_its_outputs() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_launch_that_never_compiled_taints_only_its_outputs::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_two_workflows_on_one_stream_do_not_contaminate_each_other() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_two_workflows_on_one_stream_do_not_contaminate_each_other::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_read_downstream_of_a_failure_fails_on_the_root_cause() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_read_downstream_of_a_failure_fails_on_the_root_cause::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_check_answers_without_a_read() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_check_answers_without_a_read::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_tainted_dynamic_cube_count_skips_the_launch() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_tainted_dynamic_cube_count_skips_the_launch::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_failed_in_place_launch_names_the_buffer_it_aliased() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_failed_in_place_launch_names_the_buffer_it_aliased::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_write_lands_after_the_work_queued_on_its_buffers_stream() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_write_lands_after_the_work_queued_on_its_buffers_stream::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_buffer_stays_unreadable_after_the_failure_was_reported() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_buffer_stays_unreadable_after_the_failure_was_reported::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_host_write_makes_a_stale_buffer_readable_again() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_host_write_makes_a_stale_buffer_readable_again::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_relaunch_makes_a_stale_buffer_readable_again() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_relaunch_makes_a_stale_buffer_readable_again::<
TestRuntime,
>(client);
}
#[$crate::runtime_tests::test_log::test]
fn test_a_buffer_the_failure_never_touched_reads_normally() {
let client = TestRuntime::client(&Default::default());
cubecl_core::runtime_tests::stream_errors::test_a_buffer_the_failure_never_touched_reads_normally::<
TestRuntime,
>(client);
}
}
};
}