Skip to main content

cubecl_core/frontend/
harness.rs

1//! The values a cube function's `harness` hands back to the host, see
2//! `cubecl-macros/src/generate/harness.rs`.
3
4use alloc::{boxed::Box, vec::Vec};
5use cubecl_common::bytes::Bytes;
6use cubecl_ir::ContextExt;
7use cubecl_runtime::{kernel::KernelDefinition, server::Handle};
8use variadics_please::all_tuples;
9
10use crate::prelude::*;
11use crate::{self as cubecl};
12
13/// A value a harness kernel can return to the host.
14///
15/// Implemented for every scalar, for arrays of them, for tuples of them, and
16/// for structs with `#[derive(CubeHarness)]`.
17///
18/// An array's length is comptime, so it's only known once the kernel is
19/// expanded. A harness returning one expands its kernel once before
20/// allocating, and [`__expand_store`](Self::__expand_store) records each
21/// array's length on the way, which [`alloc`](Self::alloc) and
22/// [`read`](Self::read) take back in the same order. Values without an array
23/// ignore them.
24pub trait HarnessOutput: CubeType + Sized {
25    /// The buffers a kernel stores the value into.
26    type Out: LaunchArg;
27
28    /// The value the host gets back: `Self`, except for an array, which comes
29    /// back as a `Vec`.
30    type Host;
31
32    /// Allocate the buffers, pushing each one's handle in the order [`read`](Self::read) takes
33    /// them back.
34    fn alloc(
35        client: &Client,
36        lens: &mut impl Iterator<Item = usize>,
37        handles: &mut Vec<Handle>,
38    ) -> <Self::Out as LaunchArg>::RuntimeArg;
39
40    /// Store the value into its buffers.
41    fn __expand_store(
42        scope: &Scope,
43        out: &mut <Self::Out as CubeType>::ExpandType,
44        value: Self::ExpandType,
45    );
46
47    /// Rebuild the value from the bytes of its buffers.
48    fn read(
49        bytes: &mut impl Iterator<Item = Bytes>,
50        lens: &mut impl Iterator<Item = usize>,
51    ) -> Self::Host;
52}
53
54/// The lengths of the arrays a harness kernel returns, in store order, kept
55/// in the kernel's context while it's expanded.
56#[derive(Default)]
57struct HarnessLens(Vec<usize>);
58
59/// Store a harness kernel's return value, recording its arrays' lengths.
60#[doc(hidden)]
61pub fn __expand_store_output<T: HarnessOutput>(
62    scope: &Scope,
63    out: &mut <T::Out as CubeType>::ExpandType,
64    value: T::ExpandType,
65) {
66    scope.ctx_mut().set_aux_ty(HarnessLens::default());
67    T::__expand_store(scope, out, value);
68}
69
70/// The lengths of the arrays an expanded harness kernel returns.
71#[doc(hidden)]
72pub fn __output_lens(definition: &KernelDefinition) -> Vec<usize> {
73    definition.body.ctx().aux_ty::<HarnessLens>().0.clone()
74}
75
76#[cube]
77fn store_scalar<T: CubePrimitive>(out: &mut Box<[T]>, value: T) {
78    out[0] = value;
79}
80
81impl<T: CubePrimitive + CubeElement> HarnessOutput for T {
82    type Out = Box<[T]>;
83    type Host = T;
84
85    fn alloc(
86        client: &Client,
87        _lens: &mut impl Iterator<Item = usize>,
88        handles: &mut Vec<Handle>,
89    ) -> BufferArg {
90        let handle = client.empty(core::mem::size_of::<T>());
91        handles.push(handle.clone());
92        // SAFETY: the buffer holds exactly one element.
93        unsafe { BufferArg::from_raw_parts(handle, 1) }
94    }
95
96    fn __expand_store(scope: &Scope, out: &mut NativeExpand<Box<[T]>>, value: NativeExpand<T>) {
97        store_scalar::expand::<T>(scope, out, value);
98    }
99
100    fn read(bytes: &mut impl Iterator<Item = Bytes>, _lens: &mut impl Iterator<Item = usize>) -> T {
101        let bytes = bytes.next().expect("a harness buffer for every scalar");
102        T::from_bytes(&bytes)[0]
103    }
104}
105
106#[cube]
107fn store_array<T: CubePrimitive>(out: &mut Box<[T]>, value: Array<T>) {
108    for i in 0..value.len() {
109        out[i] = value[i];
110    }
111}
112
113/// An array comes back as a `Vec` of its length.
114impl<T: CubePrimitive + CubeElement> HarnessOutput for Array<T> {
115    type Out = Box<[T]>;
116    type Host = Vec<T>;
117
118    fn alloc(
119        client: &Client,
120        lens: &mut impl Iterator<Item = usize>,
121        handles: &mut Vec<Handle>,
122    ) -> BufferArg {
123        let len = lens.next().expect("a harness length for every array");
124        // A buffer can't be empty, so an empty array still gets an element.
125        let capacity = len.max(1);
126        let handle = client.empty(capacity * core::mem::size_of::<T>());
127        handles.push(handle.clone());
128        // SAFETY: the buffer holds exactly `capacity` elements.
129        unsafe { BufferArg::from_raw_parts(handle, capacity) }
130    }
131
132    fn __expand_store(
133        scope: &Scope,
134        out: &mut NativeExpand<Box<[T]>>,
135        value: NativeExpand<Array<T>>,
136    ) {
137        let len = Array::<T>::__expand_len(scope, &value);
138        scope.ctx_mut().aux_ty_mut::<HarnessLens>().0.push(len);
139        store_array::expand::<T>(scope, out, value);
140    }
141
142    fn read(
143        bytes: &mut impl Iterator<Item = Bytes>,
144        lens: &mut impl Iterator<Item = usize>,
145    ) -> Vec<T> {
146        let bytes = bytes.next().expect("a harness buffer for every array");
147        let len = lens.next().expect("a harness length for every array");
148        T::from_bytes(&bytes)[..len].to_vec()
149    }
150}
151
152/// Nothing to hand back, for a function that only writes into its slices.
153impl HarnessOutput for () {
154    type Out = ();
155    type Host = ();
156
157    fn alloc(
158        _client: &Client,
159        _lens: &mut impl Iterator<Item = usize>,
160        _handles: &mut Vec<Handle>,
161    ) {
162    }
163
164    fn __expand_store(_scope: &Scope, _out: &mut (), _value: ()) {}
165
166    fn read(_bytes: &mut impl Iterator<Item = Bytes>, _lens: &mut impl Iterator<Item = usize>) {}
167}
168
169macro_rules! harness_tuple {
170    ($(($T:ident, $t:ident)),*) => {
171        impl<$($T: HarnessOutput),*> HarnessOutput for ($($T,)*) {
172            type Out = ($($T::Out,)*);
173            type Host = ($($T::Host,)*);
174
175            fn alloc(
176                client: &Client,
177                lens: &mut impl Iterator<Item = usize>,
178                handles: &mut Vec<Handle>,
179            ) -> <Self::Out as LaunchArg>::RuntimeArg {
180                ($($T::alloc(client, lens, handles),)*)
181            }
182
183            #[allow(non_snake_case)]
184            fn __expand_store(
185                scope: &Scope,
186                out: &mut <Self::Out as CubeType>::ExpandType,
187                value: Self::ExpandType,
188            ) {
189                let ($($T,)*) = out;
190                let ($($t,)*) = value;
191                $(<$T as HarnessOutput>::__expand_store(scope, $T, $t);)*
192            }
193
194            fn read(
195                bytes: &mut impl Iterator<Item = Bytes>,
196                lens: &mut impl Iterator<Item = usize>,
197            ) -> Self::Host {
198                ($($T::read(bytes, lens),)*)
199            }
200        }
201    };
202}
203
204all_tuples!(harness_tuple, 1, 12, T, t);