1pub(crate) mod descriptor_gate;
8
9use crate::descriptor_mapping::{
10 descriptor_bind_group, descriptor_buffer_access, descriptor_memory_kind,
11};
12use crate::WgpuBackend;
13use naga::valid::{Capabilities, ValidationFlags, Validator};
14use std::sync::Arc;
15use vyre_foundation::lower::LoweringError;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct WgpuBindingAssignment {
20 pub name: Arc<str>,
22 pub group: u32,
24 pub binding: u32,
26 pub kind: vyre_foundation::ir::MemoryKind,
28 pub access: vyre_foundation::ir::BufferAccess,
30 pub element: vyre_foundation::ir::DataType,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct WgpuDispatchGeometry {
37 pub workgroup_size: [u32; 3],
39 pub workgroups: [u32; 3],
41}
42
43#[derive(Clone, Debug)]
45pub struct WgpuProgram {
46 pub module: naga::Module,
48 pub bindings: Vec<WgpuBindingAssignment>,
50 pub workgroup_size: [u32; 3],
52 pub dispatch_geometry: WgpuDispatchGeometry,
54}
55
56#[inline]
67pub fn lower(program: &vyre_foundation::ir::Program) -> Result<String, LoweringError> {
68 lower_with_config(program, &vyre_driver::DispatchConfig::default())
69}
70
71pub fn lower_with_config(
78 program: &vyre_foundation::ir::Program,
79 config: &vyre_driver::DispatchConfig,
80) -> Result<String, LoweringError> {
81 let default_features = crate::runtime::device::EnabledFeatures::default();
82 lower_with_features(program, config, &default_features)
83}
84
85pub(crate) fn lower_with_features(
92 program: &vyre_foundation::ir::Program,
93 config: &vyre_driver::DispatchConfig,
94 enabled_features: &crate::runtime::device::EnabledFeatures,
95) -> Result<String, LoweringError> {
96 let bir = WgpuProgram::from_program(program, config, enabled_features)?;
97 write_wgsl(&bir.module)
98}
99
100pub(crate) fn optimal_workgroup_size(
109 program: &vyre_foundation::ir::Program,
110 enabled_features: &crate::runtime::device::EnabledFeatures,
111) -> [u32; 3] {
112 let requested = program.workgroup_size;
113
114 if requested != [1, 1, 1] && requested != [0, 0, 0] {
117 return requested;
118 }
119
120 let subgroup = enabled_features.min_subgroup_size.max(32);
123 let size = if program.is_explicit_noop() {
124 1
125 } else {
126 (subgroup * 4).min(256)
129 };
130
131 let max_x = enabled_features.max_workgroup_size[0].max(1);
132 [size.min(max_x), 1, 1]
133}
134
135impl WgpuProgram {
136 pub fn from_program(
143 program: &vyre_foundation::ir::Program,
144 config: &vyre_driver::DispatchConfig,
145 enabled_features: &crate::runtime::device::EnabledFeatures,
146 ) -> Result<Self, LoweringError> {
147 let mut descriptor = descriptor_gate::validate_and_analyze(program)?;
148 let workgroup_size = config
149 .workgroup_override
150 .unwrap_or_else(|| optimal_workgroup_size(program, enabled_features));
151 descriptor.dispatch.workgroup_size = workgroup_size;
152
153 if std::env::var("VYRE_DUMP_KDESC").is_ok() {
154 dump_kdesc_if_requested(&descriptor, None);
155 }
156
157 let module = match emit_naga_module_for_descriptor(&descriptor) {
158 Ok(module) => module,
159 Err(error) => {
160 if std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR").is_ok() {
161 dump_kdesc_if_requested(&descriptor, None);
162 }
163 return Err(error);
164 }
165 };
166
167 if std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR").is_ok() {
168 dump_kdesc_if_requested(&descriptor, Some(&module));
175 }
176
177 let bindings = binding_assignments(&descriptor);
178 let dispatch_geometry = WgpuDispatchGeometry {
179 workgroup_size,
180 workgroups: static_workgroups(&descriptor, workgroup_size),
181 };
182 Ok(Self {
183 module,
184 bindings,
185 workgroup_size,
186 dispatch_geometry,
187 })
188 }
189}
190
191pub(crate) fn emit_naga_module_for_descriptor(
192 descriptor: &vyre_lower::KernelDescriptor,
193) -> Result<naga::Module, LoweringError> {
194 if let Err(errors) = vyre_lower::verify::verify(descriptor) {
195 return Err(LoweringError::invalid(format!(
196 "KernelDescriptor verification failed after wgpu workgroup selection: {}. Fix: keep DispatchConfig.workgroup_override within descriptor limits.",
197 format_descriptor_verify_errors(&errors)
198 )));
199 }
200 vyre_emit_naga::emit(descriptor).map_err(|error| {
201 LoweringError::invalid(format!(
202 "KernelDescriptor Naga emission failed before wgpu WGSL writing: {error}. Fix: extend vyre-emit-naga descriptor emission; do not route around it with driver-local lowering."
203 ))
204 })
205}
206
207fn dump_kdesc_if_requested(
208 descriptor: &vyre_lower::KernelDescriptor,
209 module: Option<&naga::Module>,
210) {
211 if let Ok(dir) = std::env::var("VYRE_DUMP_KDESC")
212 .or_else(|_| std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR"))
213 {
214 let path = std::path::Path::new(&dir);
215 if let Err(error) = std::fs::create_dir_all(path) {
216 tracing::warn!(
217 "Fix: failed to create WGPU descriptor dump directory `{}`: {error}",
218 path.display()
219 );
220 return;
221 }
222 let id = &descriptor.id;
223
224 let kdesc_path = path.join(format!("{id}.kdesc.bin"));
225 match std::fs::File::create(&kdesc_path) {
226 Ok(mut file) => {
227 if let Err(error) = bincode::serde::encode_into_std_write(
228 descriptor,
229 &mut file,
230 bincode::config::standard(),
231 ) {
232 tracing::warn!(
233 "Fix: failed to serialize WGPU KernelDescriptor dump `{}`: {error}",
234 kdesc_path.display()
235 );
236 }
237 }
238 Err(error) => tracing::warn!(
239 "Fix: failed to create WGPU KernelDescriptor dump `{}`: {error}",
240 kdesc_path.display()
241 ),
242 }
243
244 if let Some(m) = module {
245 let module_path = path.join(format!("{id}.module.ron"));
246 match std::fs::File::create(&module_path) {
247 Ok(mut file) => {
248 use std::io::Write;
249 if let Err(error) = write!(file, "{m:#?}") {
250 tracing::warn!(
251 "Fix: failed to write WGPU Naga module dump `{}`: {error}",
252 module_path.display()
253 );
254 }
255 }
256 Err(error) => tracing::warn!(
257 "Fix: failed to create WGPU Naga module dump `{}`: {error}",
258 module_path.display()
259 ),
260 }
261 }
262 }
263}
264
265impl WgpuBackend {
266 pub fn lower_to_backend_ir(
268 &self,
269 program: &vyre_foundation::ir::Program,
270 ) -> Result<WgpuProgram, LoweringError> {
271 WgpuProgram::from_program(
272 program,
273 &vyre_driver::DispatchConfig::default(),
274 &self.enabled_features,
275 )
276 }
277
278 #[must_use]
283 pub fn lower_to_target<'a>(&self, bir: &'a WgpuProgram) -> &'a naga::Module {
284 &bir.module
285 }
286}
287
288fn write_wgsl(module: &naga::Module) -> Result<String, LoweringError> {
289 let mut validator = Validator::new(ValidationFlags::all(), Capabilities::all());
290 let info = match validator.validate(module) {
291 Ok(info) => info,
292 Err(e) => {
293 if let Some(func) = module.functions.iter().next() {
300 tracing::trace!(
301 target: "vyre_driver_wgpu::naga",
302 function_expressions = ?func.1.expressions,
303 "naga validation failed - function expressions",
304 );
305 }
306 if let Some(ep) = module.entry_points.first() {
307 tracing::trace!(
308 target: "vyre_driver_wgpu::naga",
309 entrypoint_expressions = ?ep.function.expressions,
310 "naga validation failed - entrypoint expressions",
311 );
312 tracing::trace!(
313 target: "vyre_driver_wgpu::naga",
314 entrypoint_locals = ?ep.function.local_variables,
315 "naga validation failed - entrypoint local variables",
316 );
317 tracing::trace!(
318 target: "vyre_driver_wgpu::naga",
319 entrypoint_body = ?ep.function.body,
320 "naga validation failed - entrypoint body",
321 );
322 }
323 return Err(LoweringError::validation(e));
324 }
325 };
326 let wgsl =
327 naga::back::wgsl::write_string(module, &info, naga::back::wgsl::WriterFlags::empty())
328 .map_err(LoweringError::writer)?;
329 const MAX_WGSL_BYTES: usize = 32 * 1024 * 1024;
340 if wgsl.len() > MAX_WGSL_BYTES {
341 return Err(LoweringError::invalid(format!(
342 "emitted WGSL is {} bytes, exceeding the {MAX_WGSL_BYTES}-byte safety cap. Fix: partition the FusionPlan into multiple megakernels (group_a / group_b / ...) with shared standard pack, or split the source Program into smaller compilation units. Adapter shader-binary-size limits are finite at scale.",
343 wgsl.len()
344 )));
345 }
346 Ok(wgsl)
347}
348
349fn binding_assignments(descriptor: &vyre_lower::KernelDescriptor) -> Vec<WgpuBindingAssignment> {
350 let mut assignments = Vec::with_capacity(descriptor.bindings.slots.len());
351 for slot in &descriptor.bindings.slots {
352 let Some(group) = descriptor_bind_group(slot.memory_class) else {
353 continue;
354 };
355 assignments.push(WgpuBindingAssignment {
356 name: Arc::from(slot.name.as_str()),
357 group,
358 binding: slot.slot,
359 kind: descriptor_memory_kind(slot.memory_class),
360 access: descriptor_buffer_access(slot.visibility),
361 element: slot.element_type.clone(),
362 });
363 }
364 assignments
365}
366
367fn static_workgroups(
368 descriptor: &vyre_lower::KernelDescriptor,
369 workgroup_size: [u32; 3],
370) -> [u32; 3] {
371 let output_words = descriptor
372 .bindings
373 .slots
374 .iter()
375 .filter(|slot| {
376 matches!(slot.memory_class, vyre_lower::MemoryClass::Global)
377 && matches!(
378 slot.visibility,
379 vyre_lower::BindingVisibility::WriteOnly
380 | vyre_lower::BindingVisibility::ReadWrite
381 )
382 })
383 .filter_map(|slot| slot.element_count)
384 .map(|count| count.max(1))
385 .max()
386 .unwrap_or(1);
387 let total_threads =
392 workgroup_size[0].max(1) * workgroup_size[1].max(1) * workgroup_size[2].max(1);
393 [output_words.div_ceil(total_threads).max(1), 1, 1]
394}
395
396fn format_descriptor_verify_errors(errors: &[vyre_lower::VerifyError]) -> String {
397 let mut out = String::new();
398 for (index, error) in errors.iter().take(4).enumerate() {
399 if index != 0 {
400 out.push_str("; ");
401 }
402 out.push_str(&format!("{error:?}"));
403 }
404 if errors.len() > 4 {
405 out.push_str("; ...");
406 }
407 out
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
414 use vyre_lower::emit_adversarial_corpus::{self, EmitAdversarialBackend};
415
416 #[test]
417 fn wgpu_program_lowers_through_kernel_descriptor() {
418 let program = Program::wrapped(
419 vec![
420 BufferDecl::storage("out", 0, BufferAccess::ReadWrite, DataType::U32)
421 .with_count(64),
422 ],
423 [1, 1, 1],
424 vec![Node::store("out", Expr::u32(0), Expr::u32(7))],
425 );
426 let mut config = vyre_driver::DispatchConfig::default();
427 config.workgroup_override = Some([32, 1, 1]);
428 let lowered = WgpuProgram::from_program(
429 &program,
430 &config,
431 &crate::runtime::device::EnabledFeatures::default(),
432 )
433 .expect("Fix: wgpu lowering must use descriptor Naga emission");
434
435 assert_eq!(lowered.workgroup_size, [32, 1, 1]);
436 assert_eq!(lowered.dispatch_geometry.workgroups, [2, 1, 1]);
437 assert_eq!(lowered.bindings.len(), 1);
438 assert_eq!(lowered.bindings[0].name.as_ref(), "out");
439 assert_eq!(lowered.bindings[0].group, 0);
440 assert_eq!(lowered.bindings[0].binding, 0);
441 }
442
443 #[test]
444 fn descriptor_binding_assignments_skip_non_resource_slots() {
445 let descriptor = vyre_lower::KernelDescriptor {
446 id: "bindings".into(),
447 bindings: vyre_lower::BindingLayout {
448 slots: vec![
449 vyre_lower::BindingSlot {
450 slot: 0,
451 element_type: DataType::U32,
452 element_count: Some(8),
453 memory_class: vyre_lower::MemoryClass::Shared,
454 visibility: vyre_lower::BindingVisibility::ReadWrite,
455 name: "scratch".to_owned(),
456 },
457 vyre_lower::BindingSlot {
458 slot: 1,
459 element_type: DataType::U32,
460 element_count: Some(8),
461 memory_class: vyre_lower::MemoryClass::Global,
462 visibility: vyre_lower::BindingVisibility::WriteOnly,
463 name: "out".to_owned(),
464 },
465 ],
466 },
467 dispatch: vyre_lower::Dispatch::new(8, 1, 1),
468 body: vyre_lower::KernelBody {
469 ops: vec![],
470 child_bodies: vec![],
471 literals: vec![],
472 },
473 };
474 let assignments = binding_assignments(&descriptor);
475 assert_eq!(assignments.len(), 1);
476 assert_eq!(assignments[0].name.as_ref(), "out");
477 assert_eq!(static_workgroups(&descriptor, [4, 1, 1]), [2, 1, 1]);
478 }
479
480 #[test]
481 fn adversarial_success_corpus_passes_wgpu_descriptor_emit_path() {
482 assert!(
483 emit_adversarial_corpus::required_backends().contains(&EmitAdversarialBackend::Wgpu),
484 "Fix: shared emit adversarial corpus must register WGPU as a required consumer."
485 );
486
487 for case in emit_adversarial_corpus::success_cases() {
488 let module =
489 emit_naga_module_for_descriptor(&case.descriptor).unwrap_or_else(|error| {
490 panic!(
491 "Fix: `{}` ({:?}) must pass WGPU descriptor emission: {}",
492 case.id,
493 case.family,
494 error.message()
495 )
496 });
497 assert_eq!(
498 module.entry_points[0].name, "main",
499 "{}: WGPU descriptor path must preserve compute entry point",
500 case.id
501 );
502 assert_eq!(
503 module.entry_points[0].workgroup_size, case.descriptor.dispatch.workgroup_size,
504 "{}: WGPU descriptor path must preserve workgroup size before adapter override",
505 case.id
506 );
507 assert!(
508 binding_assignments(&case.descriptor).len() <= case.descriptor.bindings.slots.len(),
509 "{}: WGPU binding assignment projection must not invent resource slots",
510 case.id
511 );
512 assert!(
513 static_workgroups(&case.descriptor, case.descriptor.dispatch.workgroup_size)[0]
514 >= 1,
515 "{}: WGPU static dispatch geometry must produce at least one workgroup",
516 case.id
517 );
518 }
519 }
520
521 #[test]
522 fn adversarial_rejection_corpus_returns_structured_wgpu_errors() {
523 for case in emit_adversarial_corpus::rejection_cases() {
524 let error = emit_naga_module_for_descriptor(&case.descriptor)
525 .expect_err("Fix: rejection corpus case must fail WGPU descriptor emission");
526 assert!(
527 error.message().contains("KernelDescriptor") && error.message().contains("Fix:"),
528 "Fix: `{}` WGPU descriptor rejection must include structured KernelDescriptor repair text: {}",
529 case.id,
530 error.message()
531 );
532 }
533 }
534
535 #[test]
541 fn static_workgroups_multi_dimensional_uses_total_threads() {
542 let descriptor = vyre_lower::KernelDescriptor {
543 id: "multidim".into(),
544 bindings: vyre_lower::BindingLayout {
545 slots: vec![vyre_lower::BindingSlot {
546 slot: 0,
547 element_type: DataType::U32,
548 element_count: Some(256),
549 memory_class: vyre_lower::MemoryClass::Global,
550 visibility: vyre_lower::BindingVisibility::ReadWrite,
551 name: "out".to_owned(),
552 }],
553 },
554 dispatch: vyre_lower::Dispatch::new(8, 8, 1),
555 body: vyre_lower::KernelBody {
556 ops: vec![],
557 child_bodies: vec![],
558 literals: vec![],
559 },
560 };
561 assert_eq!(static_workgroups(&descriptor, [8, 8, 1]), [4, 1, 1]);
563 assert_eq!(static_workgroups(&descriptor, [4, 4, 4]), [4, 1, 1]);
565 assert_eq!(static_workgroups(&descriptor, [16, 1, 1]), [16, 1, 1]);
567 }
568}