1use std::mem::{size_of, size_of_val};
2use std::sync::Arc;
3
4use tenferro_runtime::program::{CoreSemanticOp, SemanticOpRef, SemanticOperationView};
5use tenferro_runtime::{
6 assemble_executable_engine_registration, CoreCapabilityBundle, CoreCapabilityKind,
7 CorePrepareContext, DotGeneralPreparation, DotGeneralPrepareRequest, EngineId,
8 EngineRegistration, EngineRegistrationMetadata, ExecutableEngineRegistrationConfig,
9 ExecutionContextIdentity, HardwareClassId, InputIngressContract, InputPlacementContract,
10 InputSignature, InputSignatureContract, InputSpecializationProjection,
11 InputSpecializationRequirements, LayoutProjection, LayoutSpecialization, PrepareCapability,
12 PrepareError, PreparedOperation, PreparedOperationBinding, PreparedOperationPlan,
13 ProviderContractError, ProviderDeviceIdentity, ProviderId, ResidentOutputContract,
14 RuntimeConfigError, RuntimeInputContract, SpecializationError, SpecializationProjection,
15 SpecializationRequirements, StorageClass, UnsupportedReason,
16};
17use tenferro_tensor::{
18 AllocationDomainId, DeviceKind, GpuBackendKind, MemoryKind, Placement, TensorRead, TensorView,
19};
20
21#[cfg(not(target_family = "wasm"))]
22use super::event_domain::WebGpuEventDomainDriver;
23use super::{prepared_webgpu_view, WebGpuBackend};
24#[cfg(target_family = "wasm")]
25use tenferro_runtime::{
26 assemble_preparation_only_engine_registration, PreparationOnlyEngineRegistrationConfig,
27};
28
29const WEBGPU_ENGINE_ID: &str = "tenferro-webgpu.default.v1";
30const WEBGPU_HARDWARE_CLASS_ID: &str = "tenferro-webgpu.device.v1";
31const WEBGPU_STORAGE_CLASS_ID: &str = "tenferro.storage.device.v1";
32
33pub fn webgpu_runtime_engine_id() -> Result<EngineId, RuntimeConfigError> {
53 EngineId::new(WEBGPU_ENGINE_ID).map_err(RuntimeConfigError::from)
54}
55
56pub fn webgpu_runtime_hardware_class() -> Result<HardwareClassId, RuntimeConfigError> {
76 HardwareClassId::new(WEBGPU_HARDWARE_CLASS_ID).map_err(RuntimeConfigError::from)
77}
78
79pub fn webgpu_runtime_engine_registration(
103 backend: &WebGpuBackend,
104) -> Result<EngineRegistration, RuntimeConfigError> {
105 webgpu_runtime_engine_registration_with_id(backend, webgpu_runtime_engine_id()?)
106}
107
108pub fn webgpu_runtime_engine_registration_with_id(
119 backend: &WebGpuBackend,
120 engine_id: EngineId,
121) -> Result<EngineRegistration, RuntimeConfigError> {
122 let backend = Arc::new(backend.clone());
123 let dot_general: Arc<dyn DotGeneralPreparation> = backend.clone();
124 let execution_backend = backend.as_ref().clone();
125
126 let mut capabilities = CoreCapabilityBundle::builder();
127 capabilities.dot_general(dot_general);
128
129 let storage = webgpu_runtime_storage_class()?;
130 let default_storage = storage.clone();
131 let placement_storage = storage.clone();
132 let signature_storage = storage.clone();
133 let runtime_storage = storage.clone();
134 let resident_storage = storage.clone();
135 let runtime = backend.runtime();
136 let device_ordinal = runtime.device_ordinal();
137 let allocation_domain = runtime.allocation_domain_id();
138 let managed_domain = runtime.allocation_domain().map(|domain| domain.id);
139 let provider_device_identity = ProviderDeviceIdentity::new(
140 ProviderId::new("tenferro.webgpu")?,
141 format!("device:{device_ordinal}"),
142 )?;
143 let ingress = InputIngressContract::new(
144 InputPlacementContract::new(move |placement, candidate| {
145 candidate == &placement_storage
146 && webgpu_input_placement(placement, device_ordinal, managed_domain)
147 }),
148 InputSignatureContract::new(move |placement, family, domain, candidate| {
149 candidate == &signature_storage
150 && webgpu_input_signature(
151 placement,
152 family,
153 domain,
154 device_ordinal,
155 managed_domain,
156 allocation_domain,
157 )
158 }),
159 RuntimeInputContract::new(move |input: &TensorRead<'_>, candidate| {
160 candidate == &runtime_storage
161 && webgpu_input_tensor(input, device_ordinal, managed_domain, allocation_domain)
162 }),
163 ResidentOutputContract::new(move |input: &TensorRead<'_>, candidate| {
164 candidate == &resident_storage
165 && webgpu_input_tensor(input, device_ordinal, managed_domain, allocation_domain)
166 }),
167 );
168 let capabilities = capabilities.build();
169 #[cfg(not(target_family = "wasm"))]
170 {
171 let metadata = EngineRegistrationMetadata::new(
172 engine_id,
173 provider_device_identity,
174 webgpu_runtime_hardware_class()?,
175 Arc::from(vec![storage]),
176 default_storage,
177 capabilities,
178 );
179 assemble_executable_engine_registration(ExecutableEngineRegistrationConfig::new(
180 metadata,
181 execution_backend,
182 Arc::new(WebGpuEventDomainDriver::new(backend.runtime().clone())),
183 ingress,
184 None,
185 ))
186 }
187 #[cfg(target_family = "wasm")]
188 {
189 let metadata = EngineRegistrationMetadata::new(
190 engine_id,
191 provider_device_identity,
192 webgpu_runtime_hardware_class()?,
193 Arc::from(vec![storage]),
194 default_storage,
195 capabilities,
196 );
197 assemble_preparation_only_engine_registration(PreparationOnlyEngineRegistrationConfig::new(
198 metadata,
199 ExecutionContextIdentity::of::<WebGpuBackend>(),
200 ))
201 }
202}
203
204fn webgpu_input_signature(
205 placement: &Placement,
206 backend_family: Option<&'static str>,
207 input_domain: Option<AllocationDomainId>,
208 device_ordinal: usize,
209 managed_domain: Option<AllocationDomainId>,
210 allocation_domain: AllocationDomainId,
211) -> bool {
212 webgpu_input_placement(placement, device_ordinal, managed_domain)
213 && matches!(backend_family, Some("webgpu" | "cubecl-webgpu"))
214 && input_domain == Some(allocation_domain)
215}
216
217fn webgpu_input_placement(
218 placement: &Placement,
219 device_ordinal: usize,
220 allocation_domain: Option<AllocationDomainId>,
221) -> bool {
222 placement.memory_kind
223 == if allocation_domain.is_some() {
224 MemoryKind::Managed
225 } else {
226 MemoryKind::Device
227 }
228 && matches!(
229 &placement.device,
230 Some(device)
231 if device.kind == DeviceKind::Gpu(GpuBackendKind::WebGpu)
232 && device.ordinal == device_ordinal
233 )
234}
235
236fn webgpu_input_tensor(
237 input: &TensorRead<'_>,
238 device_ordinal: usize,
239 managed_domain: Option<AllocationDomainId>,
240 allocation_domain: AllocationDomainId,
241) -> bool {
242 webgpu_input_placement(input.placement(), device_ordinal, managed_domain)
243 && matches!(input.backend_family(), Some("webgpu" | "cubecl-webgpu"))
244 && input.allocation_domain() == Some(allocation_domain)
245 && webgpu_input_has_owned_buffer(input, device_ordinal, allocation_domain)
246}
247
248fn webgpu_input_has_owned_buffer(
249 input: &TensorRead<'_>,
250 device_ordinal: usize,
251 allocation_domain: AllocationDomainId,
252) -> bool {
253 match input.clone().tensor_view() {
254 TensorView::F32(view) => {
255 webgpu_view_has_owner::<f32>(&view, device_ordinal, allocation_domain)
256 }
257 TensorView::F64(view) => {
258 webgpu_view_has_owner::<f64>(&view, device_ordinal, allocation_domain)
259 }
260 TensorView::I32(view) => {
261 webgpu_view_has_owner::<i32>(&view, device_ordinal, allocation_domain)
262 }
263 TensorView::I64(view) => {
264 webgpu_view_has_owner::<i64>(&view, device_ordinal, allocation_domain)
265 }
266 TensorView::Bool(view) => {
267 webgpu_view_has_owner::<bool>(&view, device_ordinal, allocation_domain)
268 }
269 TensorView::C32(view) => webgpu_view_has_owner::<num_complex::Complex32>(
270 &view,
271 device_ordinal,
272 allocation_domain,
273 ),
274 TensorView::C64(view) => webgpu_view_has_owner::<num_complex::Complex64>(
275 &view,
276 device_ordinal,
277 allocation_domain,
278 ),
279 }
280}
281
282fn webgpu_view_has_owner<T: tenferro_tensor::TensorScalar + 'static>(
283 view: &tenferro_tensor::TypedTensorView<'_, T>,
284 device_ordinal: usize,
285 allocation_domain: AllocationDomainId,
286) -> bool {
287 view.backend_family()
288 .is_some_and(|family| family == "webgpu" || family == "cubecl-webgpu")
289 && view.allocation_domain() == Some(allocation_domain)
290 && matches!(
291 &view.placement().device,
292 Some(device)
293 if device.kind == DeviceKind::Gpu(GpuBackendKind::WebGpu)
294 && device.ordinal == device_ordinal
295 )
296 && prepared_webgpu_view(view, "webgpu_input_tensor")
297 .is_ok_and(|prepared| prepared.device_ordinal() == device_ordinal)
298}
299
300#[cfg(test)]
301#[path = "tests/runtime_adapter.rs"]
302mod tests;
303
304fn webgpu_runtime_storage_class() -> Result<StorageClass, RuntimeConfigError> {
305 StorageClass::new(WEBGPU_STORAGE_CLASS_ID).map_err(RuntimeConfigError::from)
306}
307
308#[derive(Debug)]
309struct WebGpuPreparedOperation {
310 binding: PreparedOperationBinding,
311 specialization: SpecializationProjection,
312}
313
314impl PreparedOperation for WebGpuPreparedOperation {
315 fn binding(&self) -> &PreparedOperationBinding {
316 &self.binding
317 }
318
319 fn specialization(&self) -> &SpecializationProjection {
320 &self.specialization
321 }
322
323 fn retained_bytes(&self) -> usize {
324 checked_specialization_heap_retained_bytes(&self.specialization).unwrap_or(usize::MAX)
325 }
326}
327
328impl DotGeneralPreparation for WebGpuBackend {
329 fn prepare(
330 &self,
331 request: DotGeneralPrepareRequest<'_>,
332 ) -> Result<PrepareCapability, PrepareError> {
333 prepare_webgpu_dot_general(request.operation(), request.context())
334 }
335}
336
337fn prepare_webgpu_dot_general(
338 operation: SemanticOperationView<'_>,
339 context: &CorePrepareContext<'_>,
340) -> Result<PrepareCapability, PrepareError> {
341 validate_webgpu_runtime_context(context)?;
342 let SemanticOpRef::Core(op) = operation.op() else {
343 return Err(wrong_family_error("extension"));
344 };
345 if !matches!(op, CoreSemanticOp::DotGeneral { .. }) {
346 return Err(wrong_family_error(core_operation_name(op)));
347 }
348
349 let minimum = dot_general_specialization_requirements(context.inputs())?;
350 let merged =
351 merge_specialization_requirements(context.specialization().requirements(), &minimum)?;
352 if &merged != context.specialization().requirements() {
353 return Ok(PrepareCapability::NeedsSpecialization(merged));
354 }
355
356 Ok(PrepareCapability::Prepared(
357 PreparedOperationPlan::metadata(Arc::new(WebGpuPreparedOperation {
358 binding: context.binding().clone(),
359 specialization: context.specialization().clone(),
360 })),
361 ))
362}
363
364fn validate_webgpu_runtime_context(context: &CorePrepareContext<'_>) -> Result<(), PrepareError> {
365 let expected_context = ExecutionContextIdentity::of::<WebGpuBackend>();
366 if context.binding().context_identity() != expected_context {
367 return Err(PrepareError::ProviderContract {
368 source: ProviderContractError::WrongOperationFamily {
369 expected: CoreCapabilityKind::DotGeneral,
370 operation: "webgpu-context-mismatch",
371 },
372 });
373 }
374 if context.binding().hardware_class().as_str() != WEBGPU_HARDWARE_CLASS_ID {
375 return Err(PrepareError::ProviderContract {
376 source: ProviderContractError::WrongOperationFamily {
377 expected: CoreCapabilityKind::DotGeneral,
378 operation: "webgpu-hardware-mismatch",
379 },
380 });
381 }
382 if context.resolved_placement().storage_class().as_str() != WEBGPU_STORAGE_CLASS_ID {
383 return Err(PrepareError::Unsupported {
384 reason: UnsupportedReason::StorageClass {
385 storage_class: context.resolved_placement().storage_class().clone(),
386 },
387 });
388 }
389 Ok(())
390}
391
392fn dot_general_specialization_requirements(
393 inputs: &InputSignature,
394) -> Result<SpecializationRequirements, PrepareError> {
395 let mut requirements = Vec::with_capacity(inputs.entries().len());
396 for (input, entry) in inputs.entries().iter().enumerate() {
397 let mut builder = InputSpecializationRequirements::builder();
398 builder
399 .dtype(true)
400 .rank(true)
401 .concrete_dimensions(concrete_axes_for_rank(input, entry.shape().len())?)
402 .layout(LayoutSpecialization::Class);
403 requirements.push(builder.build().map_err(specialization_requirements_error)?);
404 }
405 Ok(SpecializationRequirements::new(requirements))
406}
407
408fn concrete_axes_for_rank(input: usize, rank: usize) -> Result<Vec<u32>, PrepareError> {
409 if u32::try_from(rank).is_err() {
410 return Err(PrepareError::Specialization {
411 source: SpecializationError::ProjectionOverflow { input, rank },
412 });
413 }
414 let mut axes = Vec::with_capacity(rank);
415 for axis in 0..rank {
416 axes.push(
417 u32::try_from(axis).map_err(|_| PrepareError::Specialization {
418 source: SpecializationError::ProjectionOverflow { input, rank },
419 })?,
420 );
421 }
422 Ok(axes)
423}
424
425fn merge_specialization_requirements(
426 current: &SpecializationRequirements,
427 minimum: &SpecializationRequirements,
428) -> Result<SpecializationRequirements, PrepareError> {
429 debug_assert_eq!(current.inputs().len(), minimum.inputs().len());
430 let inputs = current
431 .inputs()
432 .iter()
433 .zip(minimum.inputs())
434 .map(|(current, minimum)| merge_input_requirements(current, minimum))
435 .collect::<Result<Vec<_>, _>>()?;
436 Ok(SpecializationRequirements::new(inputs))
437}
438
439fn merge_input_requirements(
440 current: &InputSpecializationRequirements,
441 minimum: &InputSpecializationRequirements,
442) -> Result<InputSpecializationRequirements, PrepareError> {
443 let mut axes = current.concrete_dimensions().to_vec();
444 for axis in minimum.concrete_dimensions() {
445 if !axes.contains(axis) {
446 axes.push(*axis);
447 }
448 }
449 let layout = current.layout().max(minimum.layout());
450 let rank = current.specializes_rank()
451 || minimum.specializes_rank()
452 || !axes.is_empty()
453 || layout == LayoutSpecialization::ExactStrides;
454 let alignment = match (current.alignment_log2(), minimum.alignment_log2()) {
455 (Some(left), Some(right)) => Some(left.max(right)),
456 (Some(value), None) | (None, Some(value)) => Some(value),
457 (None, None) => None,
458 };
459 let mut builder = InputSpecializationRequirements::builder();
460 builder
461 .dtype(current.specializes_dtype() || minimum.specializes_dtype())
462 .rank(rank)
463 .concrete_dimensions(axes)
464 .placement(current.placement().max(minimum.placement()))
465 .layout(layout)
466 .alignment_log2(alignment);
467 builder.build().map_err(specialization_requirements_error)
468}
469
470fn specialization_requirements_error(
471 source: tenferro_runtime::InputSpecializationRequirementsError,
472) -> PrepareError {
473 PrepareError::Engine {
474 source: Arc::new(source),
475 }
476}
477
478fn wrong_family_error(operation: &'static str) -> PrepareError {
479 PrepareError::ProviderContract {
480 source: ProviderContractError::WrongOperationFamily {
481 expected: CoreCapabilityKind::DotGeneral,
482 operation,
483 },
484 }
485}
486
487fn core_operation_name(op: &CoreSemanticOp) -> &'static str {
488 match op {
489 CoreSemanticOp::DotGeneral { .. } => "dot_general",
490 _ => "non-dot-general-core-operation",
491 }
492}
493
494fn checked_specialization_heap_retained_bytes(
495 specialization: &SpecializationProjection,
496) -> Option<usize> {
497 let requirements = specialization.requirements();
498 checked_sum([
499 requirements
500 .inputs()
501 .len()
502 .checked_mul(size_of::<InputSpecializationRequirements>())?,
503 checked_sum(
504 requirements
505 .inputs()
506 .iter()
507 .map(|input| size_of_val(input.concrete_dimensions())),
508 )?,
509 specialization
510 .inputs()
511 .len()
512 .checked_mul(size_of::<InputSpecializationProjection>())?,
513 checked_sum_options(
514 specialization
515 .inputs()
516 .iter()
517 .map(input_projection_retained_bytes),
518 )?,
519 ])
520}
521
522fn input_projection_retained_bytes(projection: &InputSpecializationProjection) -> Option<usize> {
523 size_of_val(projection.concrete_dimensions()).checked_add(match projection.layout() {
524 Some(LayoutProjection::ExactStrides(strides)) if strides.spilled() => {
525 size_of_val(strides.as_slice())
526 }
527 _ => 0,
528 })
529}
530
531fn checked_sum(values: impl IntoIterator<Item = usize>) -> Option<usize> {
532 values
533 .into_iter()
534 .try_fold(0usize, |sum, value| sum.checked_add(value))
535}
536
537fn checked_sum_options(values: impl IntoIterator<Item = Option<usize>>) -> Option<usize> {
538 values
539 .into_iter()
540 .try_fold(0usize, |sum, value| sum.checked_add(value?))
541}