1#![allow(
2 clippy::doc_lazy_continuation,
3 clippy::double_must_use,
4 clippy::manual_div_ceil,
5 clippy::needless_range_loop,
6 clippy::collapsible_if,
7 clippy::match_like_matches_macro,
8 clippy::redundant_closure,
9 clippy::too_many_arguments,
10 clippy::nonminimal_bool,
11 clippy::derivable_impls
12)]
13use thiserror::Error;
38use vyre_lower::KernelDescriptor;
39
40pub mod anchor_dfa_offload;
42pub mod patterns;
43
44pub use anchor_dfa_offload::{
45 SpirvAnchorDfaOffloadEvidence, SPIRV_ANCHOR_DFA_OFFLOAD_SCHEMA_VERSION,
46};
47
48#[derive(Debug, Error)]
49pub enum EmitError {
50 #[error("naga emission failed: {0}")]
51 NagaEmit(#[from] vyre_emit_naga::EmitError),
52
53 #[error("naga validation failed: {0}")]
54 NagaValidation(String),
55
56 #[error("SPIR-V writer construction failed: {0}")]
57 WriterConstruction(String),
58
59 #[error("SPIR-V writer.write failed: {0}")]
60 WriterWrite(String),
61}
62
63pub fn emit(desc: &KernelDescriptor) -> Result<Vec<u32>, EmitError> {
73 let module = vyre_emit_naga::emit(desc).map_err(EmitError::NagaEmit)?;
74 emit_from_naga_module(&module)
75}
76
77pub fn emit_optimized(desc: &KernelDescriptor) -> Result<Vec<u32>, EmitError> {
81 emit_optimized_with_stats(desc).map(|(w, _)| w)
82}
83
84pub fn emit_optimized_with_stats(
87 desc: &KernelDescriptor,
88) -> Result<(Vec<u32>, vyre_lower::rewrites::OptimizationStats), EmitError> {
89 vyre_lower::verify::verify(desc).map_err(|errors| {
95 EmitError::NagaEmit(vyre_emit_naga::EmitError::InvalidDescriptor(format!(
96 "invalid descriptor: input failed verification before optimization. {} error(s). \
97 Fix: see vyre_lower::verify for the invariants the descriptor violated. \
98 First error: {:?}",
99 errors.len(),
100 errors.first()
101 )))
102 })?;
103 let (optimized, stats) = vyre_lower::rewrites::run_all_with_stats(desc);
104 vyre_lower::verify::verify(&optimized).map_err(|errors| {
110 EmitError::NagaEmit(vyre_emit_naga::EmitError::InvalidDescriptor(format!(
111 "rewrite pipeline produced an invalid descriptor: {} error(s). \
112 Fix: see vyre_lower::verify for the invariants the rewrite violated. \
113 First error: {:?}",
114 errors.len(),
115 errors.first()
116 )))
117 })?;
118 let words = emit(&optimized)?;
119 Ok((words, stats))
120}
121
122pub fn emit_from_naga_module(module: &naga::Module) -> Result<Vec<u32>, EmitError> {
126 use naga::back::spv::{Options, PipelineOptions, Writer, WriterFlags};
127 use naga::valid::{Capabilities, ValidationFlags, Validator};
128
129 let mut validator = Validator::new(ValidationFlags::all(), Capabilities::all());
130 let info = validator
131 .validate(module)
132 .map_err(|e| EmitError::NagaValidation(format!("{e:?}")))?;
133
134 let options = Options {
135 lang_version: (1, 3),
136 capabilities: None,
137 flags: WriterFlags::empty(),
138 binding_map: Default::default(),
139 zero_initialize_workgroup_memory:
140 naga::back::spv::ZeroInitializeWorkgroupMemoryMode::Polyfill,
141 force_loop_bounding: true,
142 bounds_check_policies: naga::proc::BoundsCheckPolicies::default(),
143 debug_info: None,
144 };
145 let pipeline = PipelineOptions {
146 shader_stage: naga::ShaderStage::Compute,
147 entry_point: "main".to_string(),
148 };
149
150 let mut writer = Writer::new(&options).map_err(|e| {
151 EmitError::WriterConstruction(format!(
152 "{e:?}. Fix: upgrade naga or relax spv-out feature flags."
153 ))
154 })?;
155 let mut out = Vec::new();
156 writer
157 .write(module, &info, Some(&pipeline), &None, &mut out)
158 .map_err(|e| EmitError::WriterWrite(format!("{e:?}")))?;
159 Ok(out)
160}
161
162pub fn emit_bytes(desc: &KernelDescriptor) -> Result<Vec<u8>, EmitError> {
165 words_to_le_bytes(emit(desc)?)
166}
167
168pub fn emit_optimized_bytes(desc: &KernelDescriptor) -> Result<Vec<u8>, EmitError> {
172 words_to_le_bytes(emit_optimized(desc)?)
173}
174
175pub fn emit_optimized_bytes_with_stats(
177 desc: &KernelDescriptor,
178) -> Result<(Vec<u8>, vyre_lower::rewrites::OptimizationStats), EmitError> {
179 let (words, stats) = emit_optimized_with_stats(desc)?;
180 Ok((words_to_le_bytes(words)?, stats))
181}
182
183fn words_to_le_bytes(words: Vec<u32>) -> Result<Vec<u8>, EmitError> {
184 let mut bytes = Vec::with_capacity(words.len() * 4);
185 for word in words {
186 bytes.extend_from_slice(&word.to_le_bytes());
187 }
188 Ok(bytes)
189}
190
191pub const SPIRV_MAGIC: u32 = 0x07230203;
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use vyre_emit_naga::vyre_lower;
199 use vyre_foundation::ir::DataType;
200 use vyre_lower::{
201 BindingLayout, BindingSlot, BindingVisibility, Dispatch, KernelBody, KernelDescriptor,
202 KernelOp, KernelOpKind, LiteralValue, MemoryClass,
203 };
204
205 fn one_store_kernel() -> KernelDescriptor {
206 KernelDescriptor {
207 id: "store_one".into(),
208 bindings: BindingLayout {
209 slots: vec![BindingSlot {
210 slot: 0,
211 element_type: DataType::U32,
212 element_count: None,
213 memory_class: MemoryClass::Global,
214 visibility: BindingVisibility::ReadWrite,
215 name: "out".into(),
216 }],
217 },
218 dispatch: Dispatch::new(64, 1, 1),
219 body: KernelBody {
220 ops: vec![
221 KernelOp {
222 kind: KernelOpKind::Literal,
223 operands: vec![0],
224 result: Some(0),
225 },
226 KernelOp {
227 kind: KernelOpKind::Literal,
228 operands: vec![1],
229 result: Some(1),
230 },
231 KernelOp {
232 kind: KernelOpKind::StoreGlobal,
233 operands: vec![0, 0, 1],
234 result: None,
235 },
236 ],
237 child_bodies: vec![],
238 literals: vec![LiteralValue::U32(0), LiteralValue::U32(7)],
239 },
240 }
241 }
242
243 #[test]
244 fn empty_kernel_emits_valid_spirv_with_magic_header() {
245 let desc = KernelDescriptor {
246 id: "empty".into(),
247 bindings: BindingLayout { slots: vec![] },
248 dispatch: Dispatch::new(64, 1, 1),
249 body: KernelBody {
250 ops: vec![],
251 child_bodies: vec![],
252 literals: vec![],
253 },
254 };
255 let words = emit(&desc).unwrap();
256 assert!(!words.is_empty());
257 assert_eq!(
258 words[0], SPIRV_MAGIC,
259 "first word must be the SPIR-V magic number"
260 );
261 }
262
263 #[test]
264 fn one_store_kernel_emits_non_trivial_spirv() {
265 let words = emit(&one_store_kernel()).unwrap();
266 assert!(
267 words.len() > 16,
268 "real kernel should produce more than the header"
269 );
270 assert_eq!(words[0], SPIRV_MAGIC);
271 }
272
273 #[test]
274 fn emit_bytes_matches_words_in_le() {
275 let desc = KernelDescriptor {
276 id: "empty".into(),
277 bindings: BindingLayout { slots: vec![] },
278 dispatch: Dispatch::new(64, 1, 1),
279 body: KernelBody {
280 ops: vec![],
281 child_bodies: vec![],
282 literals: vec![],
283 },
284 };
285 let words = emit(&desc).unwrap();
286 let bytes = emit_bytes(&desc).unwrap();
287 assert_eq!(bytes.len(), words.len() * 4);
288 let first_word = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
289 assert_eq!(first_word, SPIRV_MAGIC);
290 }
291
292 #[test]
293 fn emit_optimized_bytes_produces_valid_spirv() {
294 let desc = KernelDescriptor {
295 id: "ob".into(),
296 bindings: BindingLayout { slots: vec![] },
297 dispatch: Dispatch::new(64, 1, 1),
298 body: KernelBody {
299 ops: vec![],
300 child_bodies: vec![],
301 literals: vec![],
302 },
303 };
304 let bytes = emit_optimized_bytes(&desc).unwrap();
305 assert!(bytes.len() >= 4);
306 let first_word = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
307 assert_eq!(first_word, SPIRV_MAGIC);
308 }
309
310 #[test]
311 fn emit_optimized_bytes_with_stats_returns_both() {
312 let desc = KernelDescriptor {
313 id: "obs".into(),
314 bindings: BindingLayout { slots: vec![] },
315 dispatch: Dispatch::new(64, 1, 1),
316 body: KernelBody {
317 ops: vec![],
318 child_bodies: vec![],
319 literals: vec![],
320 },
321 };
322 let (bytes, stats) = emit_optimized_bytes_with_stats(&desc).unwrap();
323 assert!(bytes.len() >= 4);
324 let first_word = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
325 assert_eq!(first_word, SPIRV_MAGIC);
326 assert!(stats.iterations >= 1);
327 }
328
329 #[test]
330 fn emit_with_unsupported_op_propagates_naga_error() {
331 let desc = KernelDescriptor {
332 id: "bad".into(),
333 bindings: BindingLayout { slots: vec![] },
334 dispatch: Dispatch::new(1, 1, 1),
335 body: KernelBody {
336 ops: vec![KernelOp {
337 kind: KernelOpKind::SubgroupReduce { op: vyre_lower::SubgroupReduceOp::Add },
338 operands: vec![0],
339 result: Some(0),
340 }],
341 child_bodies: vec![],
342 literals: vec![],
343 },
344 };
345 let r = emit(&desc);
346 assert!(matches!(r, Err(EmitError::NagaEmit(_))));
347 }
348
349 #[test]
350 fn binop_add_emits_valid_spirv() {
351 let kernel = KernelDescriptor {
352 id: "add".into(),
353 bindings: BindingLayout { slots: vec![] },
354 dispatch: Dispatch::new(1, 1, 1),
355 body: KernelBody {
356 ops: vec![
357 KernelOp {
358 kind: KernelOpKind::Literal,
359 operands: vec![0],
360 result: Some(0),
361 },
362 KernelOp {
363 kind: KernelOpKind::Literal,
364 operands: vec![1],
365 result: Some(1),
366 },
367 KernelOp {
368 kind: KernelOpKind::BinOpKind(vyre_foundation::ir::BinOp::Add),
369 operands: vec![0, 1],
370 result: Some(2),
371 },
372 ],
373 child_bodies: vec![],
374 literals: vec![LiteralValue::U32(3), LiteralValue::U32(4)],
375 },
376 };
377 let words = emit(&kernel).unwrap();
378 assert_eq!(words[0], SPIRV_MAGIC);
379 assert!(words.len() > 16);
380 }
381
382 #[test]
383 fn spirv_magic_constant_matches_spec() {
384 assert_eq!(SPIRV_MAGIC, 0x0723_0203);
385 }
386
387 #[test]
388 fn emit_optimized_succeeds_and_produces_valid_spirv() {
389 let words = emit_optimized(&one_store_kernel()).unwrap();
390 assert_eq!(words[0], SPIRV_MAGIC);
391 assert!(words.len() > 16);
392 }
393
394 #[test]
395 fn emit_optimized_drops_dead_arithmetic() {
396 use vyre_foundation::ir::BinOp as Bo;
399 let desc = KernelDescriptor {
400 id: "k".into(),
401 bindings: BindingLayout {
402 slots: vec![BindingSlot {
403 slot: 0,
404 element_type: DataType::U32,
405 element_count: None,
406 memory_class: MemoryClass::Global,
407 visibility: BindingVisibility::ReadWrite,
408 name: "buf".into(),
409 }],
410 },
411 dispatch: Dispatch::new(1, 1, 1),
412 body: KernelBody {
413 ops: vec![
414 KernelOp {
415 kind: KernelOpKind::Literal,
416 operands: vec![0],
417 result: Some(0),
418 },
419 KernelOp {
420 kind: KernelOpKind::Literal,
421 operands: vec![1],
422 result: Some(1),
423 },
424 KernelOp {
425 kind: KernelOpKind::BinOpKind(Bo::Add),
426 operands: vec![1, 0],
427 result: Some(2),
428 },
429 KernelOp {
430 kind: KernelOpKind::BinOpKind(Bo::Mul),
431 operands: vec![1, 0],
432 result: Some(3),
433 },
434 KernelOp {
435 kind: KernelOpKind::StoreGlobal,
436 operands: vec![0, 0, 1],
437 result: None,
438 },
439 ],
440 child_bodies: vec![],
441 literals: vec![LiteralValue::U32(0), LiteralValue::U32(99)],
442 },
443 };
444 let raw = emit(&desc).unwrap();
445 let optimized = emit_optimized(&desc).unwrap();
446 assert!(
447 optimized.len() <= raw.len(),
448 "optimized SPIR-V ({} words) should not exceed raw ({} words)",
449 optimized.len(),
450 raw.len()
451 );
452 }
453
454 #[test]
455 fn emit_from_naga_module_independently_consumable() {
456 let module = vyre_emit_naga::emit(&KernelDescriptor {
458 id: "k".into(),
459 bindings: BindingLayout { slots: vec![] },
460 dispatch: Dispatch::new(1, 1, 1),
461 body: KernelBody {
462 ops: vec![],
463 child_bodies: vec![],
464 literals: vec![],
465 },
466 })
467 .unwrap();
468 let words = emit_from_naga_module(&module).unwrap();
469 assert_eq!(words[0], SPIRV_MAGIC);
470 }
471
472 #[test]
480 fn emit_optimized_errors_on_invalid_rewrite_output() {
481 let bad = KernelDescriptor {
484 id: "zero_dim".into(),
485 bindings: BindingLayout { slots: vec![] },
486 dispatch: Dispatch::new(0, 1, 1),
487 body: KernelBody {
488 ops: vec![],
489 child_bodies: vec![],
490 literals: vec![],
491 },
492 };
493 let result = emit_optimized(&bad);
494 assert!(
495 result.is_err(),
496 "emit_optimized must return Err for a descriptor that fails post-rewrite verify; \
497 the old debug_assert! would silently proceed in release builds"
498 );
499 let err_msg = format!("{}", result.unwrap_err());
500 assert!(
501 err_msg.contains("invalid descriptor"),
502 "error message must mention 'invalid descriptor', got: {err_msg}"
503 );
504 }
505}