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 SubmissionEvidence, TensorExecError, TensorExecution, TensorExecutor, TensorExecutorCard,
11 TensorRequest, TensorSite, domains,
12};
13
14use crate::{
15 WgpuAdapterProbe, WgpuDiscovery, WgpuKernelDType, WgpuPipelineCache, WgpuQueueLimits,
16 WgpuResidentArena, WgpuResidentStorage, WgpuSegmentPlan, WgpuTileProfile,
17 discover_wgpu_adapters,
18 kernels::{execute_portable_kernel, kernel_op},
19};
20
21pub fn compute_wgpu_lib_symbol() -> Symbol {
23 Symbol::qualified("compute", "wgpu-lib")
24}
25
26pub fn wgpu_executor_symbol(ordinal: usize) -> Symbol {
28 Symbol::qualified("compute", format!("executor/wgpu/{ordinal}"))
29}
30
31pub fn compute_wgpu_site_symbol(ordinal: usize) -> Symbol {
33 Symbol::new(format!("site/compute/wgpu/{ordinal}"))
34}
35
36pub fn compute_wgpu_capability() -> CapabilityName {
38 CapabilityName::new("device.gpu.wgpu")
39}
40
41#[derive(Clone)]
43pub struct WgpuTensorExecutor {
44 probe: WgpuAdapterProbe,
45 state: Arc<Mutex<WgpuExecutorState>>,
46}
47
48#[derive(Debug)]
49struct WgpuExecutorState {
50 pipelines: WgpuPipelineCache,
51 arena: WgpuResidentArena,
52 queued: usize,
53 queued_bytes: u64,
54 accepted: usize,
55}
56
57impl WgpuTensorExecutor {
58 pub fn new(probe: WgpuAdapterProbe) -> Self {
60 let arena_bytes = probe.adapter.granted_limits.max_buffer_size.max(4);
61 Self {
62 probe,
63 state: Arc::new(Mutex::new(WgpuExecutorState {
64 pipelines: WgpuPipelineCache::default(),
65 arena: WgpuResidentArena::new(arena_bytes),
66 queued: 0,
67 queued_bytes: 0,
68 accepted: 0,
69 })),
70 }
71 }
72
73 pub fn probe(&self) -> &WgpuAdapterProbe {
75 &self.probe
76 }
77
78 pub fn pipeline_cache_snapshot(&self) -> crate::WgpuPipelineCacheSnapshot {
80 self.state
81 .lock()
82 .expect("wgpu executor state poisoned")
83 .pipelines
84 .snapshot()
85 }
86
87 fn dtype_for(
88 &self,
89 request: &TensorRequest,
90 ) -> std::result::Result<WgpuKernelDType, TensorExecError> {
91 let dtype = request.output.dtype();
92 if dtype == &domains::f32() || dtype == &domains::f64() {
93 Ok(WgpuKernelDType::F32)
94 } else if dtype == &domains::f16() {
95 if self.probe.adapter.granted_features.shader_f16 {
96 Ok(WgpuKernelDType::F16Native)
97 } else {
98 Ok(WgpuKernelDType::Bf16WidenedToF32)
99 }
100 } else if dtype == &domains::bf16() {
101 Ok(WgpuKernelDType::Bf16WidenedToF32)
102 } else {
103 Err(unsupported(
104 request.operation.symbol.clone(),
105 "wgpu portable kernels accept f32/f64/half-family tensor dtypes",
106 ))
107 }
108 }
109
110 fn prepare_inputs(&self, request: TensorRequest) -> TensorRequest {
111 let inputs = request
112 .inputs
113 .iter()
114 .map(|tensor| {
115 tensor
116 .storage()
117 .as_any()
118 .downcast_ref::<WgpuResidentStorage>()
119 .and_then(WgpuResidentStorage::resident_tensor)
120 .unwrap_or_else(|| tensor.clone())
121 })
122 .collect();
123 TensorRequest::new(request.operation, inputs, request.output)
124 }
125}
126
127impl TensorExecutor for WgpuTensorExecutor {
128 fn card(&self) -> TensorExecutorCard {
129 TensorExecutorCard::new(
130 wgpu_executor_symbol(self.probe.adapter.ordinal),
131 format!(
132 "wgpu/{}/{}",
133 self.probe.adapter.backend, self.probe.adapter.name
134 ),
135 Symbol::qualified("compute", "wgpu"),
136 vec![
137 sim_lib_numbers_tensor::add_op_symbol(),
138 sim_lib_numbers_tensor::sub_op_symbol(),
139 sim_lib_numbers_tensor::mul_op_symbol(),
140 sim_lib_numbers_tensor::div_op_symbol(),
141 sim_lib_numbers_tensor::sqrt_op_symbol(),
142 sim_lib_numbers_tensor::exp_op_symbol(),
143 sim_lib_numbers_tensor::sin_op_symbol(),
144 sim_lib_numbers_tensor::cos_op_symbol(),
145 sim_lib_numbers_tensor::sum_op_symbol(),
146 sim_lib_numbers_tensor::min_op_symbol(),
147 sim_lib_numbers_tensor::max_op_symbol(),
148 sim_lib_numbers_tensor::norm_op_symbol(),
149 sim_lib_numbers_tensor::transpose_exec_op_symbol(),
150 sim_lib_numbers_tensor::dot_op_symbol(),
151 sim_lib_numbers_tensor::matmul_exec_op_symbol(),
152 ],
153 Some(compute_wgpu_capability()),
154 )
155 }
156
157 fn execute(
158 &self,
159 cx: &mut sim_kernel::Cx,
160 request: TensorRequest,
161 ) -> std::result::Result<TensorExecution, TensorExecError> {
162 let Some(op) = kernel_op(&request.operation.symbol) else {
163 return Ok(TensorExecution::Unsupported {
164 reason: Arc::from("operation is outside the portable wgpu kernel set"),
165 });
166 };
167 let dtype = self.dtype_for(&request)?;
168 let request = self.prepare_inputs(request);
169 let tensor = execute_portable_kernel(cx, &request, dtype)?;
170 let bytes = tensor_bytes(tensor.shape())?;
171 let boundary = self
172 .probe
173 .adapter
174 .granted_limits
175 .max_storage_buffer_binding_size
176 .max(4);
177 let segments = WgpuSegmentPlan::new(bytes, boundary, boundary);
178 let pipeline = {
179 let mut state = self.state.lock().expect("wgpu executor state poisoned");
180 let tile = WgpuTileProfile::from_probe(&self.probe);
181 let limits = WgpuQueueLimits {
182 max_nodes: 64,
183 max_bytes: tile.max_dispatch_bytes,
184 deadline_tick: u64::MAX,
185 };
186 if state.queued >= limits.max_nodes {
187 return Err(invalid("wgpu submission queue node limit reached"));
188 }
189 if state.queued_bytes.saturating_add(bytes) > limits.max_bytes {
190 return Err(invalid("wgpu submission queue byte limit reached"));
191 }
192 let allocation = state.arena.allocate(bytes.max(4)).map_err(invalid)?;
193 state.queued += 1;
194 state.queued_bytes += bytes;
195 state.accepted += 1;
196 let pipeline =
197 state
198 .pipelines
199 .get_or_insert(&self.probe, op, dtype, tensor.shape().len());
200 (allocation, pipeline.symbol)
201 };
202 let cells = tensor.cells().map_err(TensorExecError::from)?;
203 let storage = WgpuResidentStorage::new(
204 compute_wgpu_site_symbol(self.probe.adapter.ordinal),
205 pipeline.0,
206 pipeline.1,
207 segments.segments,
208 tensor.shape().to_vec(),
209 tensor.dtype().clone(),
210 cells,
211 );
212 Ok(TensorExecution::Complete(
213 sim_lib_numbers_tensor::Tensor::from_storage(
214 tensor.shape().to_vec(),
215 tensor.dtype().clone(),
216 Arc::new(storage),
217 )?,
218 ))
219 }
220
221 fn flush(&self) -> std::result::Result<SubmissionEvidence, TensorExecError> {
222 let mut state = self.state.lock().expect("wgpu executor state poisoned");
223 let accepted = state.queued;
224 state.queued = 0;
225 state.queued_bytes = 0;
226 Ok(SubmissionEvidence::new(
227 wgpu_executor_symbol(self.probe.adapter.ordinal),
228 accepted,
229 ))
230 }
231}
232
233fn tensor_bytes(shape: &[usize]) -> std::result::Result<u64, TensorExecError> {
234 let cells = shape.iter().try_fold(1_u64, |count, extent| {
235 count
236 .checked_mul(
237 u64::try_from(*extent).map_err(|_| invalid("wgpu tensor extent exceeds u64"))?,
238 )
239 .ok_or_else(|| invalid("wgpu tensor byte count overflowed"))
240 })?;
241 cells
242 .checked_mul(4)
243 .ok_or_else(|| invalid("wgpu tensor byte count overflowed"))
244}
245
246fn invalid(message: impl Into<Arc<str>>) -> TensorExecError {
247 TensorExecError::InvalidRequest {
248 message: message.into(),
249 }
250}
251
252fn unsupported(operation: Symbol, reason: impl Into<Arc<str>>) -> TensorExecError {
253 TensorExecError::Unsupported {
254 operation,
255 reason: reason.into(),
256 }
257}
258
259#[derive(Clone, Debug, Default)]
261pub struct ComputeWgpuLib {
262 discovery: WgpuDiscovery,
263}
264
265impl ComputeWgpuLib {
266 pub fn probe() -> Result<Self> {
268 let discovery = discover_wgpu_adapters(&Default::default())
269 .map_err(|err| sim_kernel::Error::Eval(err.to_string()))?;
270 Ok(Self { discovery })
271 }
272
273 pub fn from_discovery(discovery: WgpuDiscovery) -> Self {
275 Self { discovery }
276 }
277
278 pub fn discovery(&self) -> &WgpuDiscovery {
280 &self.discovery
281 }
282}
283
284impl Lib for ComputeWgpuLib {
285 fn manifest(&self) -> LibManifest {
286 LibManifest {
287 id: compute_wgpu_lib_symbol(),
288 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
289 abi: AbiVersion { major: 0, minor: 1 },
290 target: LibTarget::HostRegistered,
291 requires: Vec::new(),
292 capabilities: vec![compute_wgpu_capability()],
293 exports: self
294 .discovery
295 .adapters
296 .iter()
297 .map(|probe| Export::Site {
298 symbol: compute_wgpu_site_symbol(probe.adapter.ordinal),
299 runtime_id: None,
300 })
301 .collect(),
302 }
303 }
304
305 fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
306 for probe in &self.discovery.adapters {
307 let symbol = compute_wgpu_site_symbol(probe.adapter.ordinal);
308 let executor = Arc::new(WgpuTensorExecutor::new(probe.clone()));
309 let site = TensorSite::new(symbol.clone(), executor, vec![compute_wgpu_capability()]);
310 linker.site_value(symbol, DefaultFactory.opaque(Arc::new(site))?)?;
311 }
312 Ok(())
313 }
314}