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 ProbePolicy, WgpuAdapterProbe, WgpuDiscovery, WgpuKernelDType, WgpuPhysicalCounters,
16 WgpuPipelineCache, WgpuProbePort, WgpuQueueLimits, WgpuResidentArena, WgpuResidentStorage,
17 WgpuResidentStorageDescriptor, WgpuSegmentPlan, WgpuTileProfile,
18 dispatch::{execute_pointwise_dispatch, is_pointwise_dispatch},
19 dispatch_linalg::execute_linalg_dispatch,
20 dispatch_reductions::execute_reduction_dispatch,
21 kernels::{execute_portable_kernel, kernel_op},
22};
23
24pub fn compute_wgpu_lib_symbol() -> Symbol {
26 Symbol::qualified("compute", "wgpu-lib")
27}
28
29pub fn wgpu_executor_symbol(ordinal: usize) -> Symbol {
31 Symbol::qualified("compute", format!("executor/wgpu/{ordinal}"))
32}
33
34pub fn compute_wgpu_site_symbol(ordinal: usize) -> Symbol {
36 Symbol::new(format!("site/compute/wgpu/{ordinal}"))
37}
38
39pub fn compute_wgpu_capability() -> CapabilityName {
41 CapabilityName::new("device.gpu.wgpu")
42}
43
44#[derive(Clone)]
46pub struct WgpuTensorExecutor {
47 pub(crate) probe: WgpuAdapterProbe,
48 pub(crate) state: Arc<Mutex<WgpuExecutorState>>,
49 pub(crate) context: Option<WgpuExecutionContext>,
50}
51
52#[derive(Debug)]
53pub(crate) struct WgpuExecutorState {
54 pub(crate) pipelines: WgpuPipelineCache,
55 arena: WgpuResidentArena,
56 queued: usize,
57 queued_bytes: u64,
58 accepted: usize,
59 physical: WgpuPhysicalCounters,
60}
61
62#[derive(Clone, Debug)]
63pub(crate) struct WgpuExecutionContext {
64 pub(crate) device: Arc<wgpu::Device>,
65 pub(crate) queue: Arc<wgpu::Queue>,
66}
67
68impl WgpuTensorExecutor {
69 pub fn new(probe: WgpuAdapterProbe) -> Self {
71 Self::from_parts(probe, None)
72 }
73
74 pub(crate) fn from_parts(
75 probe: WgpuAdapterProbe,
76 context: Option<WgpuExecutionContext>,
77 ) -> Self {
78 let arena_bytes = probe.adapter.granted_limits.max_buffer_size.max(4);
79 Self {
80 probe,
81 state: Arc::new(Mutex::new(WgpuExecutorState {
82 pipelines: WgpuPipelineCache::default(),
83 arena: WgpuResidentArena::new(arena_bytes),
84 queued: 0,
85 queued_bytes: 0,
86 accepted: 0,
87 physical: WgpuPhysicalCounters::default(),
88 })),
89 context,
90 }
91 }
92
93 pub fn probe(&self) -> &WgpuAdapterProbe {
95 &self.probe
96 }
97
98 pub fn pipeline_cache_snapshot(&self) -> crate::WgpuPipelineCacheSnapshot {
100 self.state
101 .lock()
102 .expect("wgpu executor state poisoned")
103 .pipelines
104 .snapshot()
105 }
106
107 pub fn physical_evidence(&self) -> crate::PhysicalSubmissionEvidence {
109 self.state
110 .lock()
111 .expect("wgpu executor state poisoned")
112 .physical
113 .snapshot()
114 }
115
116 pub(crate) fn physical_counters(&self) -> WgpuPhysicalCounters {
117 self.state
118 .lock()
119 .expect("wgpu executor state poisoned")
120 .physical
121 .clone()
122 }
123
124 fn dtype_for(
125 &self,
126 request: &TensorRequest,
127 ) -> std::result::Result<WgpuKernelDType, TensorExecError> {
128 let dtype = request.output.dtype();
129 if dtype == &domains::f32() || dtype == &domains::f64() {
130 Ok(WgpuKernelDType::F32)
131 } else if dtype == &domains::f16() {
132 if self.probe.adapter.granted_features.shader_f16 {
133 Ok(WgpuKernelDType::F16Native)
134 } else {
135 Ok(WgpuKernelDType::Bf16WidenedToF32)
136 }
137 } else if dtype == &domains::bf16() {
138 Ok(WgpuKernelDType::Bf16WidenedToF32)
139 } else {
140 Err(unsupported(
141 request.operation.symbol.clone(),
142 "wgpu portable kernels accept f32/f64/half-family tensor dtypes",
143 ))
144 }
145 }
146
147 fn check_submission_limits(&self, bytes: u64) -> std::result::Result<(), TensorExecError> {
148 let state = self.state.lock().expect("wgpu executor state poisoned");
149 let tile = WgpuTileProfile::from_probe(&self.probe);
150 let limits = WgpuQueueLimits {
151 max_nodes: 64,
152 max_bytes: tile.max_dispatch_bytes,
153 deadline_tick: u64::MAX,
154 };
155 if state.queued >= limits.max_nodes {
156 return Err(invalid("wgpu submission queue node limit reached"));
157 }
158 if state.queued_bytes.saturating_add(bytes) > limits.max_bytes {
159 return Err(invalid("wgpu submission queue byte limit reached"));
160 }
161 Ok(())
162 }
163}
164
165impl TensorExecutor for WgpuTensorExecutor {
166 fn card(&self) -> TensorExecutorCard {
167 TensorExecutorCard::new(
168 wgpu_executor_symbol(self.probe.adapter.ordinal),
169 format!(
170 "wgpu/{}/{}",
171 self.probe.adapter.backend, self.probe.adapter.name
172 ),
173 Symbol::qualified("compute", "wgpu"),
174 vec![
175 sim_lib_numbers_tensor::add_op_symbol(),
176 sim_lib_numbers_tensor::sub_op_symbol(),
177 sim_lib_numbers_tensor::mul_op_symbol(),
178 sim_lib_numbers_tensor::div_op_symbol(),
179 sim_lib_numbers_tensor::neg_op_symbol(),
180 sim_lib_numbers_tensor::sqrt_op_symbol(),
181 sim_lib_numbers_tensor::exp_op_symbol(),
182 Symbol::qualified("tensor", "op/log"),
183 sim_lib_numbers_tensor::sin_op_symbol(),
184 sim_lib_numbers_tensor::cos_op_symbol(),
185 sim_lib_numbers_tensor::sum_op_symbol(),
186 sim_lib_numbers_tensor::min_op_symbol(),
187 sim_lib_numbers_tensor::max_op_symbol(),
188 sim_lib_numbers_tensor::norm_op_symbol(),
189 sim_lib_numbers_tensor::transpose_exec_op_symbol(),
190 sim_lib_numbers_tensor::dot_op_symbol(),
191 sim_lib_numbers_tensor::matmul_exec_op_symbol(),
192 ],
193 Some(compute_wgpu_capability()),
194 )
195 }
196
197 fn execute(
198 &self,
199 cx: &mut sim_kernel::Cx,
200 request: TensorRequest,
201 ) -> std::result::Result<TensorExecution, TensorExecError> {
202 let Some(op) = kernel_op(&request.operation.symbol) else {
203 return Ok(TensorExecution::Unsupported {
204 reason: Arc::from("operation is outside the portable wgpu kernel set"),
205 });
206 };
207 let dtype = self.dtype_for(&request)?;
208 let bytes = tensor_bytes(request.output.shape())?;
209 self.check_submission_limits(bytes)?;
210 let dispatched = if is_pointwise_dispatch(op) {
211 Some(execute_pointwise_dispatch(
212 self, cx, &request, op, dtype, bytes,
213 )?)
214 } else if op.is_reduction() {
215 Some(execute_reduction_dispatch(self, cx, &request, op, dtype)?)
216 } else if op.is_linalg() {
217 Some(execute_linalg_dispatch(self, cx, &request, op, dtype)?)
218 } else {
219 None
220 };
221 let (buffer, pipeline_symbol, len) = if let Some(output) = dispatched {
222 (output.buffer, Some(output.pipeline), output.len)
223 } else {
224 let tensor = execute_portable_kernel(cx, &request, dtype)?;
225 let values = crate::dispatch::tensor_f32_values(cx, &tensor, dtype)?;
226 let bytes = crate::dispatch::f32_bytes(&values);
227 let Some(context) = &self.context else {
228 return Err(invalid("wgpu device context is unavailable"));
229 };
230 let buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
231 label: Some("sim-compute-wgpu-portable-upload"),
232 size: bytes.len().max(4) as u64,
233 usage: wgpu::BufferUsages::STORAGE
234 | wgpu::BufferUsages::COPY_DST
235 | wgpu::BufferUsages::COPY_SRC,
236 mapped_at_creation: false,
237 });
238 context.queue.write_buffer(&buffer, 0, &bytes);
239 self.physical_counters().record_upload(bytes.len() as u64);
240 (Arc::new(buffer), None, values.len())
241 };
242 let boundary = self
243 .probe
244 .adapter
245 .granted_limits
246 .max_storage_buffer_binding_size
247 .max(4);
248 let segments = WgpuSegmentPlan::new(bytes, boundary, boundary);
249 self.physical_counters().record_submit(&segments.segments);
250 let pipeline = {
251 let mut state = self.state.lock().expect("wgpu executor state poisoned");
252 let allocation = state.arena.allocate(bytes.max(4)).map_err(invalid)?;
253 state.queued += 1;
254 state.queued_bytes += bytes;
255 state.accepted += 1;
256 let pipeline = if let Some(pipeline_symbol) = pipeline_symbol {
257 pipeline_symbol
258 } else {
259 state
260 .pipelines
261 .get_or_insert(&self.probe, op, dtype, request.output.shape().len())
262 .symbol
263 };
264 (allocation, pipeline)
265 };
266 let storage = WgpuResidentStorage::new(WgpuResidentStorageDescriptor {
267 site: compute_wgpu_site_symbol(self.probe.adapter.ordinal),
268 allocation: pipeline.0,
269 pipeline: pipeline.1,
270 segments: segments.segments,
271 dtype: request.output.dtype().clone(),
272 len,
273 buffer,
274 context: self
275 .context
276 .clone()
277 .ok_or_else(|| invalid("wgpu device context is unavailable"))?,
278 counters: self.physical_counters(),
279 });
280 Ok(TensorExecution::Complete(
281 sim_lib_numbers_tensor::Tensor::from_storage(
282 request.output.shape().to_vec(),
283 request.output.dtype().clone(),
284 Arc::new(storage),
285 )?,
286 ))
287 }
288
289 fn flush(&self) -> std::result::Result<SubmissionEvidence, TensorExecError> {
290 let mut state = self.state.lock().expect("wgpu executor state poisoned");
291 let accepted = state.queued;
292 state.queued = 0;
293 state.queued_bytes = 0;
294 Ok(SubmissionEvidence::new(
295 wgpu_executor_symbol(self.probe.adapter.ordinal),
296 accepted,
297 ))
298 }
299}
300
301fn tensor_bytes(shape: &[usize]) -> std::result::Result<u64, TensorExecError> {
302 let cells = shape.iter().try_fold(1_u64, |count, extent| {
303 count
304 .checked_mul(
305 u64::try_from(*extent).map_err(|_| invalid("wgpu tensor extent exceeds u64"))?,
306 )
307 .ok_or_else(|| invalid("wgpu tensor byte count overflowed"))
308 })?;
309 cells
310 .checked_mul(4)
311 .ok_or_else(|| invalid("wgpu tensor byte count overflowed"))
312}
313
314fn invalid(message: impl Into<Arc<str>>) -> TensorExecError {
315 TensorExecError::InvalidRequest {
316 message: message.into(),
317 }
318}
319
320fn unsupported(operation: Symbol, reason: impl Into<Arc<str>>) -> TensorExecError {
321 TensorExecError::Unsupported {
322 operation,
323 reason: reason.into(),
324 }
325}
326
327#[derive(Clone, Debug, Default)]
329pub struct ComputeWgpuLib {
330 discovery: WgpuDiscovery,
331 contexts: Vec<WgpuExecutionContext>,
332}
333
334impl ComputeWgpuLib {
335 pub fn from_probe_port(port: &dyn WgpuProbePort, policy: &ProbePolicy) -> Result<Self> {
337 let runtimes = port
338 .probe_wgpu(policy)
339 .map_err(|err| sim_kernel::Error::Eval(err.to_string()))?;
340 let mut probes = Vec::with_capacity(runtimes.len());
341 let mut contexts = Vec::with_capacity(runtimes.len());
342 for runtime in runtimes {
343 probes.push(runtime.probe);
344 contexts.push(WgpuExecutionContext {
345 device: Arc::new(runtime.device),
346 queue: Arc::new(runtime.queue),
347 });
348 }
349 let discovery = WgpuDiscovery::from_probes(probes, Vec::new());
350 Ok(Self {
351 discovery,
352 contexts,
353 })
354 }
355
356 pub fn from_discovery(discovery: WgpuDiscovery) -> Self {
358 Self {
359 discovery,
360 contexts: Vec::new(),
361 }
362 }
363
364 pub fn discovery(&self) -> &WgpuDiscovery {
366 &self.discovery
367 }
368}
369
370impl Lib for ComputeWgpuLib {
371 fn manifest(&self) -> LibManifest {
372 LibManifest {
373 id: compute_wgpu_lib_symbol(),
374 version: Version(env!("CARGO_PKG_VERSION").to_owned()),
375 abi: AbiVersion { major: 0, minor: 1 },
376 target: LibTarget::HostRegistered,
377 requires: Vec::new(),
378 capabilities: if self.discovery.adapters.is_empty() {
379 Vec::new()
380 } else {
381 vec![compute_wgpu_capability()]
382 },
383 exports: self
384 .discovery
385 .adapters
386 .iter()
387 .map(|probe| Export::Site {
388 symbol: compute_wgpu_site_symbol(probe.adapter.ordinal),
389 runtime_id: None,
390 })
391 .collect(),
392 }
393 }
394
395 fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
396 for probe in &self.discovery.adapters {
397 let symbol = compute_wgpu_site_symbol(probe.adapter.ordinal);
398 let executor = if let Some(context) = self.contexts.get(probe.adapter.ordinal) {
399 Arc::new(WgpuTensorExecutor::from_parts(
400 probe.clone(),
401 Some(context.clone()),
402 ))
403 } else {
404 Arc::new(WgpuTensorExecutor::new(probe.clone()))
405 };
406 let site = TensorSite::new(symbol.clone(), executor, vec![compute_wgpu_capability()]);
407 linker.site_value(symbol, DefaultFactory.opaque(Arc::new(site))?)?;
408 }
409 Ok(())
410 }
411}