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 if let Err(errors) = vyre_lower::verify::verify(&descriptor) {
158 if std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR").is_ok() {
159 dump_kdesc_if_requested(&descriptor, None);
160 }
161 return Err(LoweringError::invalid(format!(
162 "KernelDescriptor verification failed after wgpu workgroup selection: {}. Fix: keep DispatchConfig.workgroup_override within descriptor limits.",
163 format_descriptor_verify_errors(&errors)
164 )));
165 }
166 let module = match vyre_emit_naga::emit(&descriptor) {
167 Ok(m) => m,
168 Err(error) => {
169 if std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR").is_ok() {
170 dump_kdesc_if_requested(&descriptor, None);
171 }
172 return Err(LoweringError::invalid(format!(
173 "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."
174 )));
175 }
176 };
177
178 if std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR").is_ok() {
179 dump_kdesc_if_requested(&descriptor, Some(&module));
186 }
187
188 let bindings = binding_assignments(&descriptor);
189 let dispatch_geometry = WgpuDispatchGeometry {
190 workgroup_size,
191 workgroups: static_workgroups(&descriptor, workgroup_size),
192 };
193 Ok(Self {
194 module,
195 bindings,
196 workgroup_size,
197 dispatch_geometry,
198 })
199 }
200}
201
202fn dump_kdesc_if_requested(
203 descriptor: &vyre_lower::KernelDescriptor,
204 module: Option<&naga::Module>,
205) {
206 if let Ok(dir) = std::env::var("VYRE_DUMP_KDESC")
207 .or_else(|_| std::env::var("VYRE_CAPTURE_FAILED_DESCRIPTOR"))
208 {
209 let path = std::path::Path::new(&dir);
210 if let Err(error) = std::fs::create_dir_all(path) {
211 tracing::warn!(
212 "Fix: failed to create WGPU descriptor dump directory `{}`: {error}",
213 path.display()
214 );
215 return;
216 }
217 let id = &descriptor.id;
218
219 let kdesc_path = path.join(format!("{id}.kdesc.bin"));
220 match std::fs::File::create(&kdesc_path) {
221 Ok(mut file) => {
222 if let Err(error) = bincode::serde::encode_into_std_write(
223 descriptor,
224 &mut file,
225 bincode::config::standard(),
226 ) {
227 tracing::warn!(
228 "Fix: failed to serialize WGPU KernelDescriptor dump `{}`: {error}",
229 kdesc_path.display()
230 );
231 }
232 }
233 Err(error) => tracing::warn!(
234 "Fix: failed to create WGPU KernelDescriptor dump `{}`: {error}",
235 kdesc_path.display()
236 ),
237 }
238
239 if let Some(m) = module {
240 let module_path = path.join(format!("{id}.module.ron"));
241 match std::fs::File::create(&module_path) {
242 Ok(mut file) => {
243 use std::io::Write;
244 if let Err(error) = write!(file, "{m:#?}") {
245 tracing::warn!(
246 "Fix: failed to write WGPU Naga module dump `{}`: {error}",
247 module_path.display()
248 );
249 }
250 }
251 Err(error) => tracing::warn!(
252 "Fix: failed to create WGPU Naga module dump `{}`: {error}",
253 module_path.display()
254 ),
255 }
256 }
257 }
258}
259
260impl WgpuBackend {
261 pub fn lower_to_backend_ir(
263 &self,
264 program: &vyre_foundation::ir::Program,
265 ) -> Result<WgpuProgram, LoweringError> {
266 WgpuProgram::from_program(
267 program,
268 &vyre_driver::DispatchConfig::default(),
269 &self.enabled_features,
270 )
271 }
272
273 #[must_use]
278 pub fn lower_to_target<'a>(&self, bir: &'a WgpuProgram) -> &'a naga::Module {
279 &bir.module
280 }
281}
282
283fn write_wgsl(module: &naga::Module) -> Result<String, LoweringError> {
284 let mut validator = Validator::new(ValidationFlags::all(), Capabilities::all());
285 let info = match validator.validate(module) {
286 Ok(info) => info,
287 Err(e) => {
288 if let Some(func) = module.functions.iter().next() {
295 tracing::trace!(
296 target: "vyre_driver_wgpu::naga",
297 function_expressions = ?func.1.expressions,
298 "naga validation failed - function expressions",
299 );
300 }
301 if let Some(ep) = module.entry_points.first() {
302 tracing::trace!(
303 target: "vyre_driver_wgpu::naga",
304 entrypoint_expressions = ?ep.function.expressions,
305 "naga validation failed - entrypoint expressions",
306 );
307 tracing::trace!(
308 target: "vyre_driver_wgpu::naga",
309 entrypoint_locals = ?ep.function.local_variables,
310 "naga validation failed - entrypoint local variables",
311 );
312 tracing::trace!(
313 target: "vyre_driver_wgpu::naga",
314 entrypoint_body = ?ep.function.body,
315 "naga validation failed - entrypoint body",
316 );
317 }
318 return Err(LoweringError::validation(e));
319 }
320 };
321 let wgsl =
322 naga::back::wgsl::write_string(module, &info, naga::back::wgsl::WriterFlags::empty())
323 .map_err(LoweringError::writer)?;
324 const MAX_WGSL_BYTES: usize = 32 * 1024 * 1024;
335 if wgsl.len() > MAX_WGSL_BYTES {
336 return Err(LoweringError::invalid(format!(
337 "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.",
338 wgsl.len()
339 )));
340 }
341 Ok(wgsl)
342}
343
344fn binding_assignments(descriptor: &vyre_lower::KernelDescriptor) -> Vec<WgpuBindingAssignment> {
345 let mut assignments = Vec::with_capacity(descriptor.bindings.slots.len());
346 for slot in &descriptor.bindings.slots {
347 let Some(group) = descriptor_bind_group(slot.memory_class) else {
348 continue;
349 };
350 assignments.push(WgpuBindingAssignment {
351 name: Arc::from(slot.name.as_str()),
352 group,
353 binding: slot.slot,
354 kind: descriptor_memory_kind(slot.memory_class),
355 access: descriptor_buffer_access(slot.visibility),
356 element: slot.element_type.clone(),
357 });
358 }
359 assignments
360}
361
362fn static_workgroups(
363 descriptor: &vyre_lower::KernelDescriptor,
364 workgroup_size: [u32; 3],
365) -> [u32; 3] {
366 let output_words = descriptor
367 .bindings
368 .slots
369 .iter()
370 .filter(|slot| {
371 matches!(slot.memory_class, vyre_lower::MemoryClass::Global)
372 && matches!(
373 slot.visibility,
374 vyre_lower::BindingVisibility::WriteOnly
375 | vyre_lower::BindingVisibility::ReadWrite
376 )
377 })
378 .filter_map(|slot| slot.element_count)
379 .map(|count| count.max(1))
380 .max()
381 .unwrap_or(1);
382 let total_threads =
387 workgroup_size[0].max(1) * workgroup_size[1].max(1) * workgroup_size[2].max(1);
388 [output_words.div_ceil(total_threads).max(1), 1, 1]
389}
390
391fn format_descriptor_verify_errors(errors: &[vyre_lower::VerifyError]) -> String {
392 let mut out = String::new();
393 for (index, error) in errors.iter().take(4).enumerate() {
394 if index != 0 {
395 out.push_str("; ");
396 }
397 out.push_str(&format!("{error:?}"));
398 }
399 if errors.len() > 4 {
400 out.push_str("; ...");
401 }
402 out
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
409
410 #[test]
411 fn wgpu_program_lowers_through_kernel_descriptor() {
412 let program = Program::wrapped(
413 vec![
414 BufferDecl::storage("out", 0, BufferAccess::ReadWrite, DataType::U32)
415 .with_count(64),
416 ],
417 [1, 1, 1],
418 vec![Node::store("out", Expr::u32(0), Expr::u32(7))],
419 );
420 let mut config = vyre_driver::DispatchConfig::default();
421 config.workgroup_override = Some([32, 1, 1]);
422 let lowered = WgpuProgram::from_program(
423 &program,
424 &config,
425 &crate::runtime::device::EnabledFeatures::default(),
426 )
427 .expect("Fix: wgpu lowering must use descriptor Naga emission");
428
429 assert_eq!(lowered.workgroup_size, [32, 1, 1]);
430 assert_eq!(lowered.dispatch_geometry.workgroups, [2, 1, 1]);
431 assert_eq!(lowered.bindings.len(), 1);
432 assert_eq!(lowered.bindings[0].name.as_ref(), "out");
433 assert_eq!(lowered.bindings[0].group, 0);
434 assert_eq!(lowered.bindings[0].binding, 0);
435 }
436
437 #[test]
438 fn descriptor_binding_assignments_skip_non_resource_slots() {
439 let descriptor = vyre_lower::KernelDescriptor {
440 id: "bindings".into(),
441 bindings: vyre_lower::BindingLayout {
442 slots: vec![
443 vyre_lower::BindingSlot {
444 slot: 0,
445 element_type: DataType::U32,
446 element_count: Some(8),
447 memory_class: vyre_lower::MemoryClass::Shared,
448 visibility: vyre_lower::BindingVisibility::ReadWrite,
449 name: "scratch".to_owned(),
450 },
451 vyre_lower::BindingSlot {
452 slot: 1,
453 element_type: DataType::U32,
454 element_count: Some(8),
455 memory_class: vyre_lower::MemoryClass::Global,
456 visibility: vyre_lower::BindingVisibility::WriteOnly,
457 name: "out".to_owned(),
458 },
459 ],
460 },
461 dispatch: vyre_lower::Dispatch::new(8, 1, 1),
462 body: vyre_lower::KernelBody {
463 ops: vec![],
464 child_bodies: vec![],
465 literals: vec![],
466 },
467 };
468 let assignments = binding_assignments(&descriptor);
469 assert_eq!(assignments.len(), 1);
470 assert_eq!(assignments[0].name.as_ref(), "out");
471 assert_eq!(static_workgroups(&descriptor, [4, 1, 1]), [2, 1, 1]);
472 }
473
474 #[test]
480 fn static_workgroups_multi_dimensional_uses_total_threads() {
481 let descriptor = vyre_lower::KernelDescriptor {
482 id: "multidim".into(),
483 bindings: vyre_lower::BindingLayout {
484 slots: vec![vyre_lower::BindingSlot {
485 slot: 0,
486 element_type: DataType::U32,
487 element_count: Some(256),
488 memory_class: vyre_lower::MemoryClass::Global,
489 visibility: vyre_lower::BindingVisibility::ReadWrite,
490 name: "out".to_owned(),
491 }],
492 },
493 dispatch: vyre_lower::Dispatch::new(8, 8, 1),
494 body: vyre_lower::KernelBody {
495 ops: vec![],
496 child_bodies: vec![],
497 literals: vec![],
498 },
499 };
500 assert_eq!(static_workgroups(&descriptor, [8, 8, 1]), [4, 1, 1]);
502 assert_eq!(static_workgroups(&descriptor, [4, 4, 4]), [4, 1, 1]);
504 assert_eq!(static_workgroups(&descriptor, [16, 1, 1]), [16, 1, 1]);
506 }
507}