1use std::sync::{Arc, Mutex};
4
5use sim_kernel::{
6 AbiVersion, CapabilityName, DefaultFactory, Export, Factory, Lib, LibManifest, LibTarget,
7 Linker, Result, Symbol, Version,
8};
9use sim_lib_numbers_tensor::{
10 CpuTensorExecutor, SubmissionEvidence, Tensor, TensorExecError, TensorExecution,
11 TensorExecutor, TensorExecutorCard, TensorRequest, TensorSite, domains, matmul_exec_op_symbol,
12 parse_f32_literal_cell,
13};
14
15use crate::{
16 CudaAbiEvidence, CudaAllocation, CudaLibrarySet, CudaLoadError, CudaProbePort,
17 CudaResidentStorage, CudaRuntimeProbe, runtime::CudaDeviceBuffer,
18};
19
20pub fn compute_cuda_lib_symbol() -> Symbol {
22 Symbol::qualified("compute", "cuda-lib")
23}
24
25pub fn cuda_executor_symbol() -> Symbol {
27 Symbol::qualified("compute", "executor/cuda")
28}
29
30pub fn compute_cuda_site_symbol() -> Symbol {
32 Symbol::new("site/compute/cuda")
33}
34
35pub fn compute_cuda_capability() -> CapabilityName {
37 CapabilityName::new("device.gpu.cuda")
38}
39
40#[derive(Clone, Debug, Default, PartialEq, Eq)]
41struct CudaExecutorState {
42 accepted: usize,
43 queued: usize,
44 next_allocation: usize,
45}
46
47#[derive(Clone)]
49pub struct CudaTensorExecutor {
50 evidence: CudaAbiEvidence,
51 runtime: Option<Arc<CudaLibrarySet>>,
52 state: Arc<Mutex<CudaExecutorState>>,
53}
54
55impl CudaTensorExecutor {
56 pub fn new(evidence: CudaAbiEvidence) -> Self {
58 Self {
59 evidence,
60 runtime: None,
61 state: Arc::new(Mutex::new(CudaExecutorState::default())),
62 }
63 }
64
65 pub fn from_runtime(runtime: Arc<CudaLibrarySet>) -> Self {
67 Self {
68 evidence: runtime.evidence().clone(),
69 runtime: Some(runtime),
70 state: Arc::new(Mutex::new(CudaExecutorState::default())),
71 }
72 }
73
74 pub fn evidence(&self) -> &CudaAbiEvidence {
76 &self.evidence
77 }
78
79 fn dtype_supported(&self, dtype: &Symbol) -> bool {
80 dtype == &domains::f32()
81 }
82
83 fn reserve_allocation(
84 &self,
85 shape: &[usize],
86 operation: Symbol,
87 ) -> std::result::Result<CudaAllocation, TensorExecError> {
88 let bytes = tensor_bytes(shape)?;
89 let mut state = self.state.lock().expect("cuda executor state poisoned");
90 state.accepted += 1;
91 state.queued += 1;
92 state.next_allocation += 1;
93 Ok(CudaAllocation {
94 id: state.next_allocation,
95 bytes,
96 operation,
97 })
98 }
99
100 fn execute_runtime(
101 &self,
102 runtime: &Arc<CudaLibrarySet>,
103 request: &TensorRequest,
104 allocation: CudaAllocation,
105 ) -> std::result::Result<TensorExecution, TensorExecError> {
106 let [left, right] = request.inputs.as_ref() else {
107 return Err(invalid("cuda matmul requires two inputs"));
108 };
109 let [rows, inner] = left.shape() else {
110 return Err(invalid("cuda matmul left input must be rank two"));
111 };
112 let [right_inner, cols] = right.shape() else {
113 return Err(invalid("cuda matmul right input must be rank two"));
114 };
115 if inner != right_inner || request.output.shape() != [*rows, *cols] {
116 return Err(invalid("cuda matmul shapes do not conform"));
117 }
118 let left = cuda_input(runtime, left)?;
119 let right = cuda_input(runtime, right)?;
120 let output = runtime
121 .matmul(&left, &right, *rows, *inner, *cols)
122 .map_err(execution_error)?;
123 let storage = CudaResidentStorage::from_device(
124 compute_cuda_site_symbol(),
125 allocation,
126 request.output.shape().to_vec(),
127 request.output.dtype().clone(),
128 output,
129 );
130 Ok(TensorExecution::Complete(Tensor::from_storage(
131 request.output.shape().to_vec(),
132 request.output.dtype().clone(),
133 Arc::new(storage),
134 )?))
135 }
136}
137
138impl TensorExecutor for CudaTensorExecutor {
139 fn card(&self) -> TensorExecutorCard {
140 TensorExecutorCard::new(
141 cuda_executor_symbol(),
142 "cuda/cublas",
143 Symbol::qualified("compute", "cuda"),
144 vec![matmul_exec_op_symbol()],
145 Some(compute_cuda_capability()),
146 )
147 }
148
149 fn execute(
150 &self,
151 cx: &mut sim_kernel::Cx,
152 request: TensorRequest,
153 ) -> std::result::Result<TensorExecution, TensorExecError> {
154 if request.operation.symbol != matmul_exec_op_symbol() {
155 return Ok(TensorExecution::Unsupported {
156 reason: Arc::from("cuda provider accepts dense matmul only"),
157 });
158 }
159 if !self.dtype_supported(request.output.dtype()) {
160 return Ok(TensorExecution::Unsupported {
161 reason: Arc::from("cuda provider accepts dense f32 matmul"),
162 });
163 }
164 let allocation =
165 self.reserve_allocation(request.output.shape(), request.operation.symbol.clone())?;
166 if let Some(runtime) = &self.runtime {
167 return self.execute_runtime(runtime, &request, allocation);
168 }
169 let result = CpuTensorExecutor::new().execute(cx, request)?;
170 let TensorExecution::Complete(tensor) = result else {
171 return Ok(result);
172 };
173 resident_result(tensor, allocation)
174 }
175
176 fn flush(&self) -> std::result::Result<SubmissionEvidence, TensorExecError> {
177 let mut state = self.state.lock().expect("cuda executor state poisoned");
178 let accepted = state.queued;
179 state.queued = 0;
180 Ok(SubmissionEvidence::new(cuda_executor_symbol(), accepted))
181 }
182}
183
184fn resident_result(
185 tensor: Tensor,
186 allocation: CudaAllocation,
187) -> std::result::Result<TensorExecution, TensorExecError> {
188 let cells = tensor.cells().map_err(TensorExecError::from)?;
189 let storage = CudaResidentStorage::new(
190 compute_cuda_site_symbol(),
191 allocation,
192 tensor.shape().to_vec(),
193 tensor.dtype().clone(),
194 cells,
195 );
196 Ok(TensorExecution::Complete(Tensor::from_storage(
197 tensor.shape().to_vec(),
198 tensor.dtype().clone(),
199 Arc::new(storage),
200 )?))
201}
202
203fn tensor_bytes(shape: &[usize]) -> std::result::Result<u64, TensorExecError> {
204 let cells = shape.iter().try_fold(1_u64, |count, extent| {
205 count
206 .checked_mul(u64::try_from(*extent).map_err(|_| invalid("cuda extent exceeds u64"))?)
207 .ok_or_else(|| invalid("cuda tensor byte count overflowed"))
208 })?;
209 cells
210 .checked_mul(4)
211 .ok_or_else(|| invalid("cuda tensor byte count overflowed"))
212}
213
214fn invalid(message: impl Into<Arc<str>>) -> TensorExecError {
215 TensorExecError::InvalidRequest {
216 message: message.into(),
217 }
218}
219
220#[derive(Clone, Debug, Default)]
223pub struct ComputeCudaLib {
224 probe: Option<CudaRuntimeProbe>,
225}
226
227impl ComputeCudaLib {
228 pub fn from_probe_port(port: &dyn CudaProbePort) -> std::result::Result<Self, CudaLoadError> {
230 Ok(Self {
231 probe: Some(port.probe_cuda()?),
232 })
233 }
234
235 pub fn from_probe(probe: CudaRuntimeProbe) -> Self {
237 Self { probe: Some(probe) }
238 }
239
240 pub fn probe_evidence(&self) -> Option<&CudaRuntimeProbe> {
242 self.probe.as_ref()
243 }
244
245 fn available_runtime(&self) -> Option<Arc<CudaLibrarySet>> {
246 self.probe
247 .as_ref()
248 .and_then(|probe| probe.runtime.clone())
249 .filter(|runtime| runtime.evidence().is_complete())
250 }
251}
252
253impl Lib for ComputeCudaLib {
254 fn manifest(&self) -> LibManifest {
255 LibManifest {
256 id: compute_cuda_lib_symbol(),
257 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
258 abi: AbiVersion { major: 0, minor: 1 },
259 target: LibTarget::HostRegistered,
260 requires: Vec::new(),
261 capabilities: self
262 .available_runtime()
263 .map(|_| vec![compute_cuda_capability()])
264 .unwrap_or_default(),
265 exports: self
266 .available_runtime()
267 .map(|_| {
268 vec![Export::Site {
269 symbol: compute_cuda_site_symbol(),
270 runtime_id: None,
271 }]
272 })
273 .unwrap_or_default(),
274 }
275 }
276
277 fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
278 let Some(runtime) = self.available_runtime() else {
279 return Ok(());
280 };
281 let executor = Arc::new(CudaTensorExecutor::from_runtime(runtime));
282 let site = TensorSite::new(
283 compute_cuda_site_symbol(),
284 executor,
285 vec![compute_cuda_capability()],
286 );
287 linker.site_value(
288 compute_cuda_site_symbol(),
289 DefaultFactory.opaque(Arc::new(site))?,
290 )?;
291 Ok(())
292 }
293}
294
295fn cuda_input(
296 runtime: &Arc<CudaLibrarySet>,
297 tensor: &Tensor,
298) -> std::result::Result<Arc<CudaDeviceBuffer>, TensorExecError> {
299 if let Some(buffer) = tensor
300 .storage()
301 .as_any()
302 .downcast_ref::<CudaResidentStorage>()
303 .and_then(CudaResidentStorage::device_buffer)
304 .filter(|buffer| Arc::ptr_eq(buffer.runtime(), runtime))
305 {
306 return Ok(Arc::clone(buffer));
307 }
308 let values = tensor
309 .cells()
310 .map_err(TensorExecError::from)?
311 .iter()
312 .map(|cell| {
313 parse_f32_literal_cell(cell)
314 .ok_or_else(|| invalid("cuda matmul input is not canonical f32"))
315 })
316 .collect::<std::result::Result<Vec<_>, _>>()?;
317 runtime.upload(&values).map_err(execution_error)
318}
319
320fn execution_error(error: CudaLoadError) -> TensorExecError {
321 TensorExecError::Eval {
322 message: Arc::from(error.to_string()),
323 }
324}