1#![cfg(feature = "cuda-oxide-backend")]
37
38use crate::ir::{ElemType, GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
39use crate::lowered_ir::{LoweredFunction, LoweredOp, LoweredType};
40
41const DEFAULT_LANES: u32 = 4;
44
45fn lowered_type_to_elem(ty: &LoweredType) -> Option<ElemType> {
53 match ty {
54 LoweredType::I8 => Some(ElemType::I8),
55 LoweredType::I16 => Some(ElemType::I16),
56 LoweredType::I32 => Some(ElemType::I32),
57 LoweredType::I64 => Some(ElemType::I64),
58 LoweredType::F32 => Some(ElemType::F32),
59 LoweredType::F64 => Some(ElemType::F64),
60 _ => None,
62 }
63}
64
65#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
67pub enum AdapterError {
68 #[error("blueprint_adapter: function does not match a wave-1 blueprint pattern ({reason})")]
72 UnmatchedPattern {
73 reason: String,
76 },
77
78 #[error("blueprint_adapter: multi-block LoweredFunction not supported in wave 1 (got {block_count} blocks)")]
81 MultiBlockNotSupported {
82 block_count: usize,
84 },
85
86 #[error("blueprint_adapter: malformed LoweredFunction")]
88 Malformed,
89}
90
91pub fn lowered_function_to_blueprint(
111 func: &LoweredFunction,
112) -> Result<TensorWasmKernelBlueprint, AdapterError> {
113 if func.blocks.is_empty() {
115 return Err(AdapterError::Malformed);
116 }
117 if func.blocks.len() > 1 {
118 return Err(AdapterError::MultiBlockNotSupported {
119 block_count: func.blocks.len(),
120 });
121 }
122
123 let block = &func.blocks[0];
124
125 let last = block.ops.last().ok_or(AdapterError::Malformed)?;
128 if !last.is_terminator() {
129 return Err(AdapterError::Malformed);
130 }
131 if !matches!(last, LoweredOp::Return { .. }) {
132 return Err(AdapterError::UnmatchedPattern {
133 reason: format!(
134 "terminator is not Return ({} ops in block)",
135 block.ops.len()
136 ),
137 });
138 }
139
140 if block.ops.len() != 4 {
145 return Err(AdapterError::UnmatchedPattern {
146 reason: format!(
147 "expected exactly 4 ops (Load, arith, Store, Return), got {}",
148 block.ops.len()
149 ),
150 });
151 }
152
153 let is_load = matches!(&block.ops[0], LoweredOp::Load { .. });
155 if !is_load {
156 return Err(AdapterError::UnmatchedPattern {
157 reason: "no Load before arith op".to_string(),
158 });
159 }
160
161 let is_store = matches!(&block.ops[2], LoweredOp::Store { .. });
163 if !is_store {
164 return Err(AdapterError::UnmatchedPattern {
165 reason: "no Store after arith op".to_string(),
166 });
167 }
168
169 let unsupported_elem = |ty: &LoweredType| AdapterError::UnmatchedPattern {
173 reason: format!("arith op element type `{ty}` has no blueprint representation"),
174 };
175 let (kernel_op, elem) = match &block.ops[1] {
176 LoweredOp::AddF { ty, .. } | LoweredOp::AddI { ty, .. } => {
177 let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
178 (
179 TensorWasmOp::VecAdd {
180 elem,
181 lanes: DEFAULT_LANES,
182 },
183 elem,
184 )
185 }
186 LoweredOp::MulF { ty, .. } | LoweredOp::MulI { ty, .. } => {
187 let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
188 (
189 TensorWasmOp::VecMul {
190 elem,
191 lanes: DEFAULT_LANES,
192 },
193 elem,
194 )
195 }
196 LoweredOp::Fma { ty, .. } => {
197 let elem = lowered_type_to_elem(ty).ok_or_else(|| unsupported_elem(ty))?;
198 (
199 TensorWasmOp::VecFma {
200 elem,
201 lanes: DEFAULT_LANES,
202 },
203 elem,
204 )
205 }
206 other => {
210 return Err(AdapterError::UnmatchedPattern {
211 reason: format!(
212 "unsupported middle op: {} (wave 1 recognises Add/Mul/Fma only; \
213 MatMul deferred to wave 3)",
214 debug_op_name(other)
215 ),
216 });
217 }
218 };
219
220 let blueprint = TensorWasmKernelBlueprint {
223 entry: func.name.clone(),
224 ops: vec![
225 TensorWasmOp::LoadUnified {
226 elem,
227 lanes: DEFAULT_LANES,
228 },
229 kernel_op,
230 TensorWasmOp::StoreUnified {
231 elem,
232 lanes: DEFAULT_LANES,
233 },
234 ],
235 grid_hint: GridHint::default(),
236 shared_mem_bytes: 0,
237 };
238
239 Ok(blueprint)
240}
241
242fn debug_op_name(op: &LoweredOp) -> &'static str {
246 match op {
247 LoweredOp::AddI { .. } => "AddI",
248 LoweredOp::SubI { .. } => "SubI",
249 LoweredOp::MulI { .. } => "MulI",
250 LoweredOp::DivS { .. } => "DivS",
251 LoweredOp::DivU { .. } => "DivU",
252 LoweredOp::RemS { .. } => "RemS",
253 LoweredOp::RemU { .. } => "RemU",
254 LoweredOp::AddF { .. } => "AddF",
255 LoweredOp::SubF { .. } => "SubF",
256 LoweredOp::MulF { .. } => "MulF",
257 LoweredOp::DivF { .. } => "DivF",
258 LoweredOp::Fma { .. } => "Fma",
259 LoweredOp::FNeg { .. } => "FNeg",
260 LoweredOp::FAbs { .. } => "FAbs",
261 LoweredOp::Load { .. } => "Load",
262 LoweredOp::Store { .. } => "Store",
263 LoweredOp::StackAlloc { .. } => "StackAlloc",
264 LoweredOp::Br { .. } => "Br",
265 LoweredOp::CondBr { .. } => "CondBr",
266 LoweredOp::Switch { .. } => "Switch",
267 LoweredOp::Return { .. } => "Return",
268 LoweredOp::VMin { .. } => "VMin",
269 LoweredOp::VMax { .. } => "VMax",
270 LoweredOp::VSplat { .. } => "VSplat",
271 LoweredOp::VSelect { .. } => "VSelect",
272 LoweredOp::VAllTrue { .. } => "VAllTrue",
273 LoweredOp::VAnyTrue { .. } => "VAnyTrue",
274 LoweredOp::Select { .. } => "Select",
275 LoweredOp::Bitcast { .. } => "Bitcast",
276 LoweredOp::TruncI { .. } => "TruncI",
277 LoweredOp::ExtendU { .. } => "ExtendU",
278 LoweredOp::ExtendS { .. } => "ExtendS",
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::lowered_ir::{
286 LoweredBlock, LoweredFunction, LoweredOp, LoweredSignature, LoweredType,
287 };
288
289 fn straight_line_fn(name: &str, mid: LoweredOp) -> LoweredFunction {
291 let mut func = LoweredFunction::new(
292 name,
293 LoweredSignature {
294 params: vec![LoweredType::Ptr, LoweredType::Ptr],
295 returns: vec![],
296 },
297 );
298 let mut block = LoweredBlock::new(0);
299 block.params = vec![(1, LoweredType::Ptr), (2, LoweredType::Ptr)];
300 block.ops.push(LoweredOp::Load {
301 ty: LoweredType::F32,
302 base: 1,
303 offset: 0,
304 result: 10,
305 });
306 block.ops.push(mid);
307 block.ops.push(LoweredOp::Store {
308 ty: LoweredType::F32,
309 value: 11,
310 base: 2,
311 offset: 0,
312 });
313 block.ops.push(LoweredOp::Return { values: vec![] });
314 func.blocks.push(block);
315 func
316 }
317
318 #[test]
319 fn load_addf_store_becomes_vec_add() {
320 let func = straight_line_fn(
321 "vector_add",
322 LoweredOp::AddF {
323 ty: LoweredType::F32,
324 lhs: 10,
325 rhs: 10,
326 result: 11,
327 },
328 );
329 let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
330 assert_eq!(bp.entry, "vector_add");
331 assert_eq!(
332 bp.ops,
333 vec![
334 TensorWasmOp::LoadUnified {
335 elem: ElemType::F32,
336 lanes: 4
337 },
338 TensorWasmOp::VecAdd {
339 elem: ElemType::F32,
340 lanes: 4
341 },
342 TensorWasmOp::StoreUnified {
343 elem: ElemType::F32,
344 lanes: 4
345 },
346 ]
347 );
348 assert_eq!(bp.shared_mem_bytes, 0);
349 assert_eq!(bp.grid_hint, GridHint::default());
350 }
351
352 #[test]
353 fn load_addi_store_also_becomes_vec_add() {
354 let func = straight_line_fn(
355 "int_add",
356 LoweredOp::AddI {
357 ty: LoweredType::I32,
358 lhs: 10,
359 rhs: 10,
360 result: 11,
361 },
362 );
363 let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
364 assert!(matches!(
365 bp.ops[1],
366 TensorWasmOp::VecAdd {
367 elem: ElemType::F32,
368 lanes: 4
369 }
370 ));
371 }
372
373 #[test]
374 fn load_mulf_store_becomes_vec_mul() {
375 let func = straight_line_fn(
376 "vector_mul",
377 LoweredOp::MulF {
378 ty: LoweredType::F32,
379 lhs: 10,
380 rhs: 10,
381 result: 11,
382 },
383 );
384 let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
385 assert_eq!(bp.entry, "vector_mul");
386 assert!(matches!(
387 bp.ops[1],
388 TensorWasmOp::VecMul {
389 elem: ElemType::F32,
390 lanes: 4
391 }
392 ));
393 }
394
395 #[test]
396 fn load_muli_store_also_becomes_vec_mul() {
397 let func = straight_line_fn(
398 "int_mul",
399 LoweredOp::MulI {
400 ty: LoweredType::I32,
401 lhs: 10,
402 rhs: 10,
403 result: 11,
404 },
405 );
406 let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
407 assert!(matches!(
408 bp.ops[1],
409 TensorWasmOp::VecMul {
410 elem: ElemType::F32,
411 lanes: 4
412 }
413 ));
414 }
415
416 #[test]
417 fn load_fma_store_becomes_vec_fma() {
418 let func = straight_line_fn(
419 "vector_fma",
420 LoweredOp::Fma {
421 ty: LoweredType::F32,
422 a: 10,
423 b: 10,
424 c: 10,
425 result: 11,
426 },
427 );
428 let bp = lowered_function_to_blueprint(&func).expect("should produce blueprint");
429 assert_eq!(bp.entry, "vector_fma");
430 assert!(matches!(
431 bp.ops[1],
432 TensorWasmOp::VecFma {
433 elem: ElemType::F32,
434 lanes: 4
435 }
436 ));
437 }
438
439 #[test]
440 fn multi_block_rejected() {
441 let mut func = LoweredFunction::new(
442 "multi",
443 LoweredSignature {
444 params: vec![],
445 returns: vec![],
446 },
447 );
448 let mut b0 = LoweredBlock::new(0);
450 b0.ops.push(LoweredOp::Br {
451 target: 1,
452 args: vec![],
453 });
454 let mut b1 = LoweredBlock::new(1);
455 b1.ops.push(LoweredOp::Return { values: vec![] });
456 func.blocks.push(b0);
457 func.blocks.push(b1);
458
459 let err = lowered_function_to_blueprint(&func).unwrap_err();
460 assert_eq!(err, AdapterError::MultiBlockNotSupported { block_count: 2 });
461 }
462
463 #[test]
464 fn unsupported_pattern_rejected_with_descriptive_reason() {
465 let func = straight_line_fn(
469 "weird",
470 LoweredOp::Bitcast {
471 from_ty: LoweredType::F32,
472 to_ty: LoweredType::I32,
473 src: 10,
474 result: 11,
475 },
476 );
477 let err = lowered_function_to_blueprint(&func).unwrap_err();
478 match err {
479 AdapterError::UnmatchedPattern { reason } => {
480 assert!(
481 reason.contains("Bitcast"),
482 "reason should name the offending op, got: {reason}"
483 );
484 }
485 other => panic!("expected UnmatchedPattern, got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn empty_function_is_malformed() {
491 let func = LoweredFunction::new("empty", LoweredSignature::default());
492 let err = lowered_function_to_blueprint(&func).unwrap_err();
493 assert_eq!(err, AdapterError::Malformed);
494 }
495
496 #[test]
497 fn block_without_terminator_is_malformed() {
498 let mut func = LoweredFunction::new("bad", LoweredSignature::default());
500 func.blocks.push(LoweredBlock::new(0));
501 let err = lowered_function_to_blueprint(&func).unwrap_err();
502 assert_eq!(err, AdapterError::Malformed);
503 }
504
505 #[test]
506 fn extra_intermediate_op_rejected() {
507 let mut func = LoweredFunction::new(
511 "extra",
512 LoweredSignature {
513 params: vec![LoweredType::Ptr],
514 returns: vec![],
515 },
516 );
517 let mut block = LoweredBlock::new(0);
518 block.ops.push(LoweredOp::Load {
519 ty: LoweredType::F32,
520 base: 1,
521 offset: 0,
522 result: 10,
523 });
524 block.ops.push(LoweredOp::AddF {
525 ty: LoweredType::F32,
526 lhs: 10,
527 rhs: 10,
528 result: 11,
529 });
530 block.ops.push(LoweredOp::Bitcast {
531 from_ty: LoweredType::F32,
532 to_ty: LoweredType::I32,
533 src: 11,
534 result: 12,
535 });
536 block.ops.push(LoweredOp::Store {
537 ty: LoweredType::I32,
538 value: 12,
539 base: 1,
540 offset: 0,
541 });
542 block.ops.push(LoweredOp::Return { values: vec![] });
543 func.blocks.push(block);
544
545 let err = lowered_function_to_blueprint(&func).unwrap_err();
546 assert!(matches!(err, AdapterError::UnmatchedPattern { .. }));
547 }
548
549 #[test]
550 fn adapter_error_displays_useful_messages() {
551 let e = AdapterError::MultiBlockNotSupported { block_count: 3 };
555 let s = e.to_string();
556 assert!(s.contains("multi-block"));
557 assert!(s.contains('3'));
558
559 let e = AdapterError::UnmatchedPattern {
560 reason: "no Load before arith op".to_string(),
561 };
562 let s = e.to_string();
563 assert!(s.contains("no Load before arith op"));
564 }
565}