1use super::{GpuBackend, GpuError, GpuTapeData, TapeMeta};
6
7pub struct WgpuTapeBuffers {
9 pub(crate) opcodes_buf: wgpu::Buffer,
10 pub(crate) arg0_buf: wgpu::Buffer,
11 pub(crate) arg1_buf: wgpu::Buffer,
12 pub(crate) constants_buf: wgpu::Buffer,
13 pub(crate) output_indices_buf: wgpu::Buffer,
14 pub(crate) num_ops: u32,
15 pub(crate) num_inputs: u32,
16 pub(crate) num_variables: u32,
17 pub(crate) num_outputs: u32,
18}
19
20pub struct WgpuContext {
22 device: wgpu::Device,
23 queue: wgpu::Queue,
24 forward_pipeline: wgpu::ComputePipeline,
25 reverse_pipeline: wgpu::ComputePipeline,
26 tangent_fwd_pipeline: wgpu::ComputePipeline,
27 tangent_rev_pipeline: wgpu::ComputePipeline,
28 #[cfg(feature = "stde")]
30 taylor_fwd_kth_pipelines: [wgpu::ComputePipeline; 5],
31 tape_bind_group_layout: wgpu::BindGroupLayout,
32 forward_io_bind_group_layout: wgpu::BindGroupLayout,
33 reverse_io_bind_group_layout: wgpu::BindGroupLayout,
34 tangent_fwd_io_bind_group_layout: wgpu::BindGroupLayout,
35 tangent_rev_io_bind_group_layout: wgpu::BindGroupLayout,
36 #[cfg(feature = "stde")]
37 taylor_fwd_2nd_io_bind_group_layout: wgpu::BindGroupLayout,
38}
39
40impl WgpuContext {
41 #[must_use]
43 pub fn new() -> Option<Self> {
44 pollster::block_on(Self::new_async())
45 }
46
47 async fn new_async() -> Option<Self> {
48 let instance = wgpu::Instance::default();
49 let adapter = instance
50 .request_adapter(&wgpu::RequestAdapterOptions {
51 power_preference: wgpu::PowerPreference::HighPerformance,
52 compatible_surface: None,
53 force_fallback_adapter: false,
54 })
55 .await
56 .ok()?;
57
58 let required_limits = wgpu::Limits {
63 max_storage_buffers_per_shader_stage: 13,
64 ..wgpu::Limits::downlevel_defaults()
65 }
66 .using_resolution(adapter.limits());
67 let (device, queue) = adapter
68 .request_device(&wgpu::DeviceDescriptor {
69 required_limits,
70 ..wgpu::DeviceDescriptor::default()
71 })
72 .await
73 .ok()?;
74
75 let tape_bind_group_layout =
77 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
78 label: Some("echidna_tape_bgl"),
79 entries: &[
80 bgl_storage_ro(0),
82 bgl_storage_ro(1),
84 bgl_storage_ro(2),
86 bgl_storage_ro(3),
88 wgpu::BindGroupLayoutEntry {
90 binding: 4,
91 visibility: wgpu::ShaderStages::COMPUTE,
92 ty: wgpu::BindingType::Buffer {
93 ty: wgpu::BufferBindingType::Uniform,
94 has_dynamic_offset: false,
95 min_binding_size: None,
96 },
97 count: None,
98 },
99 bgl_storage_ro(5),
101 ],
102 });
103
104 let forward_io_bind_group_layout =
106 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
107 label: Some("echidna_fwd_io_bgl"),
108 entries: &[
109 bgl_storage_ro(0),
111 bgl_storage_rw(1),
113 bgl_storage_rw(2),
115 ],
116 });
117
118 let reverse_io_bind_group_layout =
120 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
121 label: Some("echidna_rev_io_bgl"),
122 entries: &[
123 bgl_storage_ro(0),
125 bgl_storage_rw(1),
127 bgl_storage_rw(2),
129 ],
130 });
131
132 let fwd_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
134 label: Some("echidna_forward_pl"),
135 bind_group_layouts: &[
136 Some(&tape_bind_group_layout),
137 Some(&forward_io_bind_group_layout),
138 ],
139 immediate_size: 0,
140 });
141
142 let fwd_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
143 label: Some("echidna_forward_shader"),
144 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/forward.wgsl").into()),
145 });
146
147 let forward_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
148 label: Some("echidna_forward_pipeline"),
149 layout: Some(&fwd_layout),
150 module: &fwd_shader,
151 entry_point: Some("main"),
152 compilation_options: Default::default(),
153 cache: None,
154 });
155
156 let rev_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
158 label: Some("echidna_reverse_pl"),
159 bind_group_layouts: &[
160 Some(&tape_bind_group_layout),
161 Some(&reverse_io_bind_group_layout),
162 ],
163 immediate_size: 0,
164 });
165
166 let rev_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
167 label: Some("echidna_reverse_shader"),
168 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/reverse.wgsl").into()),
169 });
170
171 let reverse_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
172 label: Some("echidna_reverse_pipeline"),
173 layout: Some(&rev_layout),
174 module: &rev_shader,
175 entry_point: Some("main"),
176 compilation_options: Default::default(),
177 cache: None,
178 });
179
180 let tangent_fwd_io_bind_group_layout =
182 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
183 label: Some("echidna_tfwd_io_bgl"),
184 entries: &[
185 bgl_storage_ro(0), bgl_storage_ro(1), bgl_storage_rw(2), bgl_storage_rw(3), bgl_storage_rw(4), ],
191 });
192
193 let tfwd_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
194 label: Some("echidna_tangent_fwd_pl"),
195 bind_group_layouts: &[
196 Some(&tape_bind_group_layout),
197 Some(&tangent_fwd_io_bind_group_layout),
198 ],
199 immediate_size: 0,
200 });
201
202 let tfwd_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
203 label: Some("echidna_tangent_fwd_shader"),
204 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/tangent_forward.wgsl").into()),
205 });
206
207 let tangent_fwd_pipeline =
208 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
209 label: Some("echidna_tangent_fwd_pipeline"),
210 layout: Some(&tfwd_layout),
211 module: &tfwd_shader,
212 entry_point: Some("main"),
213 compilation_options: Default::default(),
214 cache: None,
215 });
216
217 let tangent_rev_io_bind_group_layout =
219 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
220 label: Some("echidna_trev_io_bgl"),
221 entries: &[
222 bgl_storage_ro(0), bgl_storage_ro(1), bgl_storage_rw(2), bgl_storage_rw(3), bgl_storage_rw(4), bgl_storage_rw(5), bgl_storage_rw(6), bgl_storage_rw(7), ],
231 });
232
233 let trev_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
234 label: Some("echidna_tangent_rev_pl"),
235 bind_group_layouts: &[
236 Some(&tape_bind_group_layout),
237 Some(&tangent_rev_io_bind_group_layout),
238 ],
239 immediate_size: 0,
240 });
241
242 let trev_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
243 label: Some("echidna_tangent_rev_shader"),
244 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/tangent_reverse.wgsl").into()),
245 });
246
247 let tangent_rev_pipeline =
248 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
249 label: Some("echidna_tangent_rev_pipeline"),
250 layout: Some(&trev_layout),
251 module: &trev_shader,
252 entry_point: Some("main"),
253 compilation_options: Default::default(),
254 cache: None,
255 });
256
257 #[cfg(feature = "stde")]
259 let taylor_fwd_2nd_io_bind_group_layout =
260 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
261 label: Some("echidna_taylor2_io_bgl"),
262 entries: &[
263 bgl_storage_ro(0), bgl_storage_ro(1), bgl_storage_rw(2), bgl_storage_rw(3), ],
268 });
269
270 #[cfg(feature = "stde")]
271 let taylor2_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
272 label: Some("echidna_taylor_fwd_2nd_pl"),
273 bind_group_layouts: &[
274 Some(&tape_bind_group_layout),
275 Some(&taylor_fwd_2nd_io_bind_group_layout),
276 ],
277 immediate_size: 0,
278 });
279
280 #[cfg(feature = "stde")]
283 let taylor_fwd_kth_pipelines = {
284 use super::taylor_codegen::generate_taylor_wgsl;
285 std::array::from_fn(|idx| {
286 let k = idx + 1;
287 let wgsl_src = generate_taylor_wgsl(k);
288 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
289 label: Some(&format!("echidna_taylor_fwd_k{k}_shader")),
290 source: wgpu::ShaderSource::Wgsl(wgsl_src.into()),
291 });
292 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
293 label: Some(&format!("echidna_taylor_fwd_k{k}_pipeline")),
294 layout: Some(&taylor2_layout),
295 module: &shader,
296 entry_point: Some("main"),
297 compilation_options: Default::default(),
298 cache: None,
299 })
300 })
301 };
302
303 Some(WgpuContext {
304 device,
305 queue,
306 forward_pipeline,
307 reverse_pipeline,
308 tangent_fwd_pipeline,
309 tangent_rev_pipeline,
310 #[cfg(feature = "stde")]
311 taylor_fwd_kth_pipelines,
312 tape_bind_group_layout,
313 forward_io_bind_group_layout,
314 reverse_io_bind_group_layout,
315 tangent_fwd_io_bind_group_layout,
316 tangent_rev_io_bind_group_layout,
317 #[cfg(feature = "stde")]
318 taylor_fwd_2nd_io_bind_group_layout,
319 })
320 }
321
322 fn create_tape_bind_group(
324 &self,
325 tape: &WgpuTapeBuffers,
326 meta_buf: &wgpu::Buffer,
327 ) -> wgpu::BindGroup {
328 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
329 label: Some("tape_bg"),
330 layout: &self.tape_bind_group_layout,
331 entries: &[
332 wgpu::BindGroupEntry {
333 binding: 0,
334 resource: tape.opcodes_buf.as_entire_binding(),
335 },
336 wgpu::BindGroupEntry {
337 binding: 1,
338 resource: tape.arg0_buf.as_entire_binding(),
339 },
340 wgpu::BindGroupEntry {
341 binding: 2,
342 resource: tape.arg1_buf.as_entire_binding(),
343 },
344 wgpu::BindGroupEntry {
345 binding: 3,
346 resource: tape.constants_buf.as_entire_binding(),
347 },
348 wgpu::BindGroupEntry {
349 binding: 4,
350 resource: meta_buf.as_entire_binding(),
351 },
352 wgpu::BindGroupEntry {
353 binding: 5,
354 resource: tape.output_indices_buf.as_entire_binding(),
355 },
356 ],
357 })
358 }
359
360 #[cfg(feature = "stde")]
370 pub fn taylor_forward_kth_batch(
371 &self,
372 tape: &WgpuTapeBuffers,
373 primal_inputs: &[f32],
374 direction_seeds: &[f32],
375 batch_size: u32,
376 order: usize,
377 ) -> Result<super::TaylorKthBatchResult<f32>, GpuError> {
378 use wgpu::util::DeviceExt;
379
380 if !(1..=5).contains(&order) {
381 return Err(GpuError::Other(format!(
382 "unsupported Taylor order {order}, must be 1..=5"
383 )));
384 }
385
386 let k = order as u32;
388 let ni = tape.num_inputs;
389 let nv = tape.num_variables;
390 let no = tape.num_outputs;
391
392 assert!(
397 (batch_size as u64) * (nv as u64) * (k as u64) <= u32::MAX as u64,
398 "batch_size * num_variables * order overflows u32 in WGSL shader index arithmetic"
399 );
400 let total_inputs = (batch_size as usize) * (ni as usize);
401
402 assert_eq!(
403 primal_inputs.len(),
404 total_inputs,
405 "primal_inputs length mismatch"
406 );
407 assert_eq!(
408 direction_seeds.len(),
409 total_inputs,
410 "direction_seeds length mismatch"
411 );
412
413 let meta = TapeMeta {
414 num_ops: tape.num_ops,
415 num_inputs: ni,
416 num_variables: nv,
417 num_outputs: no,
418 batch_size,
419 _pad: [0; 3],
420 };
421 let meta_buf = self
422 .device
423 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
424 label: Some("taylor_kth_meta"),
425 contents: bytemuck::bytes_of(&meta),
426 usage: wgpu::BufferUsages::UNIFORM,
427 });
428
429 let primal_buf = self
430 .device
431 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
432 label: Some("taylor_kth_primals"),
433 contents: bytemuck::cast_slice(primal_inputs),
434 usage: wgpu::BufferUsages::STORAGE,
435 });
436 let seed_buf = self
437 .device
438 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
439 label: Some("taylor_kth_seeds"),
440 contents: bytemuck::cast_slice(direction_seeds),
441 usage: wgpu::BufferUsages::STORAGE,
442 });
443
444 let jets_size = (batch_size as u64) * (nv as u64) * (k as u64) * 4;
446 let jets_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
447 label: Some("taylor_kth_jets"),
448 size: jets_size,
449 usage: wgpu::BufferUsages::STORAGE,
450 mapped_at_creation: false,
451 });
452
453 let out_count = (batch_size as u64) * (no as u64) * (k as u64);
455 let out_size = out_count * 4;
456 let jet_out_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
457 label: Some("taylor_kth_jet_out"),
458 size: out_size,
459 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
460 mapped_at_creation: false,
461 });
462
463 let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
464 label: Some("taylor_kth_staging"),
465 size: out_size,
466 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
467 mapped_at_creation: false,
468 });
469
470 let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
471
472 let io_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
473 label: Some("taylor_kth_io_bg"),
474 layout: &self.taylor_fwd_2nd_io_bind_group_layout,
475 entries: &[
476 wgpu::BindGroupEntry {
477 binding: 0,
478 resource: primal_buf.as_entire_binding(),
479 },
480 wgpu::BindGroupEntry {
481 binding: 1,
482 resource: seed_buf.as_entire_binding(),
483 },
484 wgpu::BindGroupEntry {
485 binding: 2,
486 resource: jets_buf.as_entire_binding(),
487 },
488 wgpu::BindGroupEntry {
489 binding: 3,
490 resource: jet_out_buf.as_entire_binding(),
491 },
492 ],
493 });
494
495 let mut encoder = self
496 .device
497 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
498 label: Some("taylor_kth_enc"),
499 });
500
501 {
502 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
503 label: Some("taylor_kth_pass"),
504 timestamp_writes: None,
505 });
506 pass.set_pipeline(&self.taylor_fwd_kth_pipelines[order - 1]);
507 pass.set_bind_group(0, &tape_bg, &[]);
508 pass.set_bind_group(1, &io_bg, &[]);
509 self.check_dispatch_1d(batch_size.div_ceil(256))?;
510 pass.dispatch_workgroups(batch_size.div_ceil(256), 1, 1);
511 }
512
513 encoder.copy_buffer_to_buffer(&jet_out_buf, 0, &staging_buf, 0, out_size);
514 let sub_idx = self.queue.submit(std::iter::once(encoder.finish()));
515
516 let slice = staging_buf.slice(..);
517 let (tx, rx) = std::sync::mpsc::channel();
518 slice.map_async(wgpu::MapMode::Read, move |result| {
519 let _ = tx.send(result);
520 });
521 self.device
526 .poll(wgpu::PollType::Wait {
527 submission_index: Some(sub_idx),
528 timeout: None,
529 })
530 .map_err(|e| GpuError::Other(format!("device poll failed: {e}")))?;
531
532 rx.recv()
533 .map_err(|e| GpuError::Other(format!("channel recv failed: {e}")))?
534 .map_err(|e| GpuError::Other(format!("buffer map failed: {e}")))?;
535
536 let data = slice.get_mapped_range();
537 let raw: &[f32] = bytemuck::cast_slice(&data);
538
539 let total_out = (batch_size as usize) * (no as usize);
541 let mut coefficients: Vec<Vec<f32>> =
542 (0..order).map(|_| Vec::with_capacity(total_out)).collect();
543
544 for i in 0..total_out {
545 for c in 0..order {
546 coefficients[c].push(raw[i * order + c]);
547 }
548 }
549
550 drop(data);
551 staging_buf.unmap();
552
553 Ok(super::TaylorKthBatchResult {
554 coefficients,
555 order,
556 })
557 }
558}
559
560impl WgpuContext {
561 fn check_dispatch_1d(&self, workgroups: u32) -> Result<(), GpuError> {
566 let max = self.device.limits().max_compute_workgroups_per_dimension;
567 if workgroups > max {
568 return Err(GpuError::Other(format!(
569 "dispatch of {workgroups} workgroups exceeds the device limit of {max} \
570 per dimension (~{} elements at 256 per workgroup); split the batch",
571 (max as u64) * 256
572 )));
573 }
574 Ok(())
575 }
576}
577
578impl GpuBackend for WgpuContext {
579 type TapeBuffers = WgpuTapeBuffers;
580
581 fn num_outputs(&self, tape: &WgpuTapeBuffers) -> u32 {
582 tape.num_outputs
583 }
584
585 fn upload_tape(&self, data: &GpuTapeData) -> WgpuTapeBuffers {
586 use wgpu::util::DeviceExt;
587
588 if let Err(e) = data.validate() {
589 panic!("refusing to upload invalid GpuTapeData: {e}");
590 }
591
592 let opcodes_buf = self
593 .device
594 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
595 label: Some("opcodes"),
596 contents: bytemuck::cast_slice(&data.opcodes),
597 usage: wgpu::BufferUsages::STORAGE,
598 });
599 let arg0_buf = self
600 .device
601 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
602 label: Some("arg0"),
603 contents: bytemuck::cast_slice(&data.arg0),
604 usage: wgpu::BufferUsages::STORAGE,
605 });
606 let arg1_buf = self
607 .device
608 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
609 label: Some("arg1"),
610 contents: bytemuck::cast_slice(&data.arg1),
611 usage: wgpu::BufferUsages::STORAGE,
612 });
613 let constants_buf = self
614 .device
615 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
616 label: Some("constants"),
617 contents: bytemuck::cast_slice(&data.constants),
618 usage: wgpu::BufferUsages::STORAGE,
619 });
620
621 let num_outputs = if data.output_indices.is_empty() {
622 1u32
623 } else {
624 data.output_indices.len() as u32
625 };
626 let output_indices = if data.output_indices.is_empty() {
627 vec![data.output_index]
628 } else {
629 data.output_indices.clone()
630 };
631
632 let output_indices_buf =
633 self.device
634 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
635 label: Some("output_indices"),
636 contents: bytemuck::cast_slice(&output_indices),
637 usage: wgpu::BufferUsages::STORAGE,
638 });
639
640 WgpuTapeBuffers {
641 opcodes_buf,
642 arg0_buf,
643 arg1_buf,
644 constants_buf,
645 output_indices_buf,
646 num_ops: data.num_ops,
647 num_inputs: data.num_inputs,
648 num_variables: data.num_variables,
649 num_outputs,
650 }
651 }
652
653 fn forward_batch(
661 &self,
662 tape: &WgpuTapeBuffers,
663 inputs: &[f32],
664 batch_size: u32,
665 ) -> Result<Vec<f32>, GpuError> {
666 use wgpu::util::DeviceExt;
667
668 let num_inputs = tape.num_inputs;
669 let num_variables = tape.num_variables;
670 let num_outputs = tape.num_outputs;
671
672 assert_eq!(
673 inputs.len(),
674 (batch_size as usize) * (num_inputs as usize),
675 "inputs length must be batch_size * num_inputs"
676 );
677
678 assert!(
680 (batch_size as u64) * (num_variables as u64) <= u32::MAX as u64,
681 "batch_size * num_variables overflows u32 in WGSL shader index arithmetic"
682 );
683
684 let meta = TapeMeta {
686 num_ops: tape.num_ops,
687 num_inputs,
688 num_variables,
689 num_outputs,
690 batch_size,
691 _pad: [0; 3],
692 };
693 let meta_buf = self
694 .device
695 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
696 label: Some("tape_meta"),
697 contents: bytemuck::bytes_of(&meta),
698 usage: wgpu::BufferUsages::UNIFORM,
699 });
700
701 let input_buf = self
703 .device
704 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
705 label: Some("inputs"),
706 contents: bytemuck::cast_slice(inputs),
707 usage: wgpu::BufferUsages::STORAGE,
708 });
709
710 let values_size = (batch_size as u64) * (num_variables as u64) * 4;
712 let values_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
713 label: Some("values"),
714 size: values_size,
715 usage: wgpu::BufferUsages::STORAGE,
716 mapped_at_creation: false,
717 });
718
719 let output_size = (batch_size as u64) * (num_outputs as u64) * 4;
721 let output_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
722 label: Some("outputs"),
723 size: output_size,
724 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
725 mapped_at_creation: false,
726 });
727
728 let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
730 label: Some("staging"),
731 size: output_size,
732 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
733 mapped_at_creation: false,
734 });
735
736 let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
738
739 let io_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
741 label: Some("io_bg"),
742 layout: &self.forward_io_bind_group_layout,
743 entries: &[
744 wgpu::BindGroupEntry {
745 binding: 0,
746 resource: input_buf.as_entire_binding(),
747 },
748 wgpu::BindGroupEntry {
749 binding: 1,
750 resource: values_buf.as_entire_binding(),
751 },
752 wgpu::BindGroupEntry {
753 binding: 2,
754 resource: output_buf.as_entire_binding(),
755 },
756 ],
757 });
758
759 let mut encoder = self
761 .device
762 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
763 label: Some("forward_enc"),
764 });
765
766 {
767 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
768 label: Some("forward_pass"),
769 timestamp_writes: None,
770 });
771 pass.set_pipeline(&self.forward_pipeline);
772 pass.set_bind_group(0, &tape_bg, &[]);
773 pass.set_bind_group(1, &io_bg, &[]);
774 self.check_dispatch_1d(batch_size.div_ceil(256))?;
775 pass.dispatch_workgroups(batch_size.div_ceil(256), 1, 1);
776 }
777
778 encoder.copy_buffer_to_buffer(&output_buf, 0, &staging_buf, 0, output_size);
779 let sub_idx = self.queue.submit(std::iter::once(encoder.finish()));
780
781 let slice = staging_buf.slice(..);
783 let (tx, rx) = std::sync::mpsc::channel();
784 slice.map_async(wgpu::MapMode::Read, move |result| {
785 let _ = tx.send(result);
786 });
787 self.device
792 .poll(wgpu::PollType::Wait {
793 submission_index: Some(sub_idx),
794 timeout: None,
795 })
796 .map_err(|e| GpuError::Other(format!("device poll failed: {e}")))?;
797
798 rx.recv()
799 .map_err(|e| GpuError::Other(format!("channel recv failed: {e}")))?
800 .map_err(|e| GpuError::Other(format!("buffer map failed: {e}")))?;
801
802 let data = slice.get_mapped_range();
803 let result: Vec<f32> = bytemuck::cast_slice(&data).to_vec();
804 drop(data);
805 staging_buf.unmap();
806
807 Ok(result)
808 }
809
810 fn gradient_batch(
818 &self,
819 tape: &WgpuTapeBuffers,
820 inputs: &[f32],
821 batch_size: u32,
822 ) -> Result<(Vec<f32>, Vec<f32>), GpuError> {
823 use wgpu::util::DeviceExt;
824
825 let num_inputs = tape.num_inputs;
826 let num_variables = tape.num_variables;
827 let num_outputs = tape.num_outputs;
828
829 assert_eq!(
830 inputs.len(),
831 (batch_size as usize) * (num_inputs as usize),
832 "inputs length must be batch_size * num_inputs"
833 );
834
835 assert!(
837 (batch_size as u64) * (num_variables as u64) <= u32::MAX as u64,
838 "batch_size * num_variables overflows u32 in WGSL shader index arithmetic"
839 );
840
841 let meta = TapeMeta {
843 num_ops: tape.num_ops,
844 num_inputs,
845 num_variables,
846 num_outputs,
847 batch_size,
848 _pad: [0; 3],
849 };
850 let meta_buf = self
851 .device
852 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
853 label: Some("tape_meta"),
854 contents: bytemuck::bytes_of(&meta),
855 usage: wgpu::BufferUsages::UNIFORM,
856 });
857
858 let input_buf = self
860 .device
861 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
862 label: Some("inputs"),
863 contents: bytemuck::cast_slice(inputs),
864 usage: wgpu::BufferUsages::STORAGE,
865 });
866
867 let values_size = (batch_size as u64) * (num_variables as u64) * 4;
869 let values_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
870 label: Some("values"),
871 size: values_size,
872 usage: wgpu::BufferUsages::STORAGE,
873 mapped_at_creation: false,
874 });
875
876 let output_count = (batch_size as u64) * (num_outputs as u64);
878 let output_size = output_count * 4;
879 let output_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
880 label: Some("outputs"),
881 size: output_size,
882 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
883 mapped_at_creation: false,
884 });
885
886 let adjoints_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
888 label: Some("adjoints"),
889 size: values_size,
890 usage: wgpu::BufferUsages::STORAGE,
891 mapped_at_creation: false,
892 });
893
894 let grad_count = (batch_size as u64) * (num_inputs as u64);
896 let grad_size = grad_count * 4;
897 let grad_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
898 label: Some("grad_out"),
899 size: grad_size,
900 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
901 mapped_at_creation: false,
902 });
903
904 let output_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
906 label: Some("output_staging"),
907 size: output_size,
908 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
909 mapped_at_creation: false,
910 });
911 let grad_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
912 label: Some("grad_staging"),
913 size: grad_size,
914 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
915 mapped_at_creation: false,
916 });
917
918 let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
920
921 let fwd_io_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
923 label: Some("fwd_io_bg"),
924 layout: &self.forward_io_bind_group_layout,
925 entries: &[
926 wgpu::BindGroupEntry {
927 binding: 0,
928 resource: input_buf.as_entire_binding(),
929 },
930 wgpu::BindGroupEntry {
931 binding: 1,
932 resource: values_buf.as_entire_binding(),
933 },
934 wgpu::BindGroupEntry {
935 binding: 2,
936 resource: output_buf.as_entire_binding(),
937 },
938 ],
939 });
940
941 let rev_io_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
943 label: Some("rev_io_bg"),
944 layout: &self.reverse_io_bind_group_layout,
945 entries: &[
946 wgpu::BindGroupEntry {
947 binding: 0,
948 resource: values_buf.as_entire_binding(),
949 },
950 wgpu::BindGroupEntry {
951 binding: 1,
952 resource: adjoints_buf.as_entire_binding(),
953 },
954 wgpu::BindGroupEntry {
955 binding: 2,
956 resource: grad_buf.as_entire_binding(),
957 },
958 ],
959 });
960
961 let workgroups = batch_size.div_ceil(256);
962 self.check_dispatch_1d(workgroups)?;
963
964 let mut encoder = self
966 .device
967 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
968 label: Some("gradient_enc"),
969 });
970
971 {
973 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
974 label: Some("forward_pass"),
975 timestamp_writes: None,
976 });
977 pass.set_pipeline(&self.forward_pipeline);
978 pass.set_bind_group(0, &tape_bg, &[]);
979 pass.set_bind_group(1, &fwd_io_bg, &[]);
980 pass.dispatch_workgroups(workgroups, 1, 1);
981 }
982
983 {
985 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
986 label: Some("reverse_pass"),
987 timestamp_writes: None,
988 });
989 pass.set_pipeline(&self.reverse_pipeline);
990 pass.set_bind_group(0, &tape_bg, &[]);
991 pass.set_bind_group(1, &rev_io_bg, &[]);
992 pass.dispatch_workgroups(workgroups, 1, 1);
993 }
994
995 encoder.copy_buffer_to_buffer(&output_buf, 0, &output_staging, 0, output_size);
996 encoder.copy_buffer_to_buffer(&grad_buf, 0, &grad_staging, 0, grad_size);
997 let sub_idx = self.queue.submit(std::iter::once(encoder.finish()));
998
999 let out_slice = output_staging.slice(..);
1001 let grad_slice = grad_staging.slice(..);
1002
1003 let (tx1, rx1) = std::sync::mpsc::channel();
1004 let (tx2, rx2) = std::sync::mpsc::channel();
1005 out_slice.map_async(wgpu::MapMode::Read, move |r| {
1006 let _ = tx1.send(r);
1007 });
1008 grad_slice.map_async(wgpu::MapMode::Read, move |r| {
1009 let _ = tx2.send(r);
1010 });
1011
1012 self.device
1017 .poll(wgpu::PollType::Wait {
1018 submission_index: Some(sub_idx),
1019 timeout: None,
1020 })
1021 .map_err(|e| GpuError::Other(format!("device poll failed: {e}")))?;
1022
1023 rx1.recv()
1024 .map_err(|e| GpuError::Other(format!("channel recv failed: {e}")))?
1025 .map_err(|e| GpuError::Other(format!("output map failed: {e}")))?;
1026 rx2.recv()
1027 .map_err(|e| GpuError::Other(format!("channel recv failed: {e}")))?
1028 .map_err(|e| GpuError::Other(format!("grad map failed: {e}")))?;
1029
1030 let out_data = out_slice.get_mapped_range();
1031 let outputs: Vec<f32> = bytemuck::cast_slice(&out_data).to_vec();
1032 drop(out_data);
1033 output_staging.unmap();
1034
1035 let grad_data = grad_slice.get_mapped_range();
1036 let grads: Vec<f32> = bytemuck::cast_slice(&grad_data).to_vec();
1037 drop(grad_data);
1038 grad_staging.unmap();
1039
1040 Ok((outputs, grads))
1041 }
1042 fn sparse_jacobian(
1055 &self,
1056 tape: &WgpuTapeBuffers,
1057 tape_cpu: &mut crate::BytecodeTape<f32>,
1058 x: &[f32],
1059 ) -> Result<(Vec<f32>, crate::sparse::JacobianSparsityPattern, Vec<f32>), GpuError> {
1060 use wgpu::util::DeviceExt;
1061
1062 let num_inputs = tape.num_inputs as usize;
1063 let num_outputs = tape.num_outputs as usize;
1064 let num_variables = tape.num_variables;
1065
1066 let pattern = tape_cpu.detect_jacobian_sparsity();
1068 let (colors, num_colors) = crate::sparse::column_coloring(&pattern);
1069
1070 if num_colors == 0 {
1071 tape_cpu.forward(x);
1073 let vals = tape_cpu.output_values();
1074 let vals_f32: Vec<f32> = vals.to_vec();
1075 return Ok((vals_f32, pattern, vec![]));
1076 }
1077
1078 assert!(
1083 (num_colors as u64) * (num_variables as u64) <= u32::MAX as u64,
1084 "num_colors * num_variables overflows u32 in WGSL shader index arithmetic"
1085 );
1086
1087 let batch = num_colors;
1090 let mut primal_inputs = Vec::with_capacity(batch as usize * num_inputs);
1091 let mut tangent_seeds = Vec::with_capacity(batch as usize * num_inputs);
1092
1093 for c in 0..num_colors {
1094 for i in 0..num_inputs {
1095 primal_inputs.push(x[i]);
1096 tangent_seeds.push(if colors[i] == c { 1.0f32 } else { 0.0f32 });
1097 }
1098 }
1099
1100 let meta = TapeMeta {
1102 num_ops: tape.num_ops,
1103 num_inputs: tape.num_inputs,
1104 num_variables,
1105 num_outputs: tape.num_outputs,
1106 batch_size: batch,
1107 _pad: [0; 3],
1108 };
1109 let meta_buf = self
1110 .device
1111 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1112 label: Some("tape_meta"),
1113 contents: bytemuck::bytes_of(&meta),
1114 usage: wgpu::BufferUsages::UNIFORM,
1115 });
1116
1117 let primal_buf = self
1118 .device
1119 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1120 label: Some("primal_inputs"),
1121 contents: bytemuck::cast_slice(&primal_inputs),
1122 usage: wgpu::BufferUsages::STORAGE,
1123 });
1124 let seed_buf = self
1125 .device
1126 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1127 label: Some("tangent_seeds"),
1128 contents: bytemuck::cast_slice(&tangent_seeds),
1129 usage: wgpu::BufferUsages::STORAGE,
1130 });
1131
1132 let buf_size = (batch as u64) * (num_variables as u64) * 4;
1133 let primals_work = self.device.create_buffer(&wgpu::BufferDescriptor {
1134 label: Some("primals_work"),
1135 size: buf_size,
1136 usage: wgpu::BufferUsages::STORAGE,
1137 mapped_at_creation: false,
1138 });
1139 let tangents_work = self.device.create_buffer(&wgpu::BufferDescriptor {
1140 label: Some("tangents_work"),
1141 size: buf_size,
1142 usage: wgpu::BufferUsages::STORAGE,
1143 mapped_at_creation: false,
1144 });
1145
1146 let out_size = (batch as u64) * (tape.num_outputs as u64) * 4;
1147 let tangent_out_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1148 label: Some("tangent_outputs"),
1149 size: out_size,
1150 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
1151 mapped_at_creation: false,
1152 });
1153 let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
1154 label: Some("staging"),
1155 size: out_size,
1156 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1157 mapped_at_creation: false,
1158 });
1159
1160 let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
1161
1162 let io_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1163 label: Some("tfwd_io_bg"),
1164 layout: &self.tangent_fwd_io_bind_group_layout,
1165 entries: &[
1166 wgpu::BindGroupEntry {
1167 binding: 0,
1168 resource: primal_buf.as_entire_binding(),
1169 },
1170 wgpu::BindGroupEntry {
1171 binding: 1,
1172 resource: seed_buf.as_entire_binding(),
1173 },
1174 wgpu::BindGroupEntry {
1175 binding: 2,
1176 resource: primals_work.as_entire_binding(),
1177 },
1178 wgpu::BindGroupEntry {
1179 binding: 3,
1180 resource: tangents_work.as_entire_binding(),
1181 },
1182 wgpu::BindGroupEntry {
1183 binding: 4,
1184 resource: tangent_out_buf.as_entire_binding(),
1185 },
1186 ],
1187 });
1188
1189 let mut encoder = self
1190 .device
1191 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1192 label: Some("sparse_jac_enc"),
1193 });
1194 {
1195 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1196 label: Some("tangent_fwd_pass"),
1197 timestamp_writes: None,
1198 });
1199 pass.set_pipeline(&self.tangent_fwd_pipeline);
1200 pass.set_bind_group(0, &tape_bg, &[]);
1201 pass.set_bind_group(1, &io_bg, &[]);
1202 self.check_dispatch_1d(batch.div_ceil(256))?;
1203 pass.dispatch_workgroups(batch.div_ceil(256), 1, 1);
1204 }
1205 encoder.copy_buffer_to_buffer(&tangent_out_buf, 0, &staging, 0, out_size);
1206 let sub_idx = self.queue.submit(std::iter::once(encoder.finish()));
1207
1208 let slice = staging.slice(..);
1209 let (tx, rx) = std::sync::mpsc::channel();
1210 slice.map_async(wgpu::MapMode::Read, move |r| {
1211 let _ = tx.send(r);
1212 });
1213 self.device
1218 .poll(wgpu::PollType::Wait {
1219 submission_index: Some(sub_idx),
1220 timeout: None,
1221 })
1222 .map_err(|e| GpuError::Other(format!("device poll failed: {e}")))?;
1223 rx.recv()
1224 .map_err(|e| GpuError::Other(format!("recv: {e}")))?
1225 .map_err(|e| GpuError::Other(format!("map: {e}")))?;
1226
1227 let data = slice.get_mapped_range();
1228 let tangent_results: Vec<f32> = bytemuck::cast_slice(&data).to_vec();
1229 drop(data);
1230 staging.unmap();
1231
1232 let nnz = pattern.nnz();
1236 let mut jac_values = vec![0.0f32; nnz];
1237
1238 for (k, (&row, &col)) in pattern.rows.iter().zip(pattern.cols.iter()).enumerate() {
1239 let o = row as usize; let i = col as usize; let c = colors[i] as usize;
1242 jac_values[k] = tangent_results[c * num_outputs + o];
1243 }
1244
1245 tape_cpu.forward(x);
1247 let output_values: Vec<f32> = tape_cpu.output_values();
1248
1249 Ok((output_values, pattern, jac_values))
1250 }
1251
1252 fn hvp_batch(
1261 &self,
1262 tape: &WgpuTapeBuffers,
1263 x: &[f32],
1264 tangent_dirs: &[f32],
1265 batch_size: u32,
1266 ) -> Result<(Vec<f32>, Vec<f32>), GpuError> {
1267 use wgpu::util::DeviceExt;
1268
1269 let ni = tape.num_inputs;
1270 let nv = tape.num_variables;
1271
1272 assert_eq!(x.len(), ni as usize);
1273 assert_eq!(tangent_dirs.len(), (batch_size as usize) * (ni as usize));
1274
1275 assert!(
1277 (batch_size as u64) * (nv as u64) <= u32::MAX as u64,
1278 "batch_size * num_variables overflows u32 in WGSL shader index arithmetic"
1279 );
1280
1281 let mut primal_inputs = Vec::with_capacity((batch_size as usize) * (ni as usize));
1283 for _ in 0..batch_size {
1284 primal_inputs.extend_from_slice(x);
1285 }
1286
1287 let meta = TapeMeta {
1288 num_ops: tape.num_ops,
1289 num_inputs: ni,
1290 num_variables: nv,
1291 num_outputs: tape.num_outputs,
1292 batch_size,
1293 _pad: [0; 3],
1294 };
1295
1296 let meta_buf = self
1297 .device
1298 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1299 label: Some("meta"),
1300 contents: bytemuck::bytes_of(&meta),
1301 usage: wgpu::BufferUsages::UNIFORM,
1302 });
1303 let primal_buf = self
1304 .device
1305 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1306 label: Some("primals_in"),
1307 contents: bytemuck::cast_slice(&primal_inputs),
1308 usage: wgpu::BufferUsages::STORAGE,
1309 });
1310 let seed_buf = self
1311 .device
1312 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1313 label: Some("seeds"),
1314 contents: bytemuck::cast_slice(tangent_dirs),
1315 usage: wgpu::BufferUsages::STORAGE,
1316 });
1317
1318 let buf_size = (batch_size as u64) * (nv as u64) * 4;
1319 let grad_size = (batch_size as u64) * (ni as u64) * 4;
1320
1321 let primals_work = self.device.create_buffer(&wgpu::BufferDescriptor {
1322 label: Some("pw"),
1323 size: buf_size,
1324 usage: wgpu::BufferUsages::STORAGE,
1325 mapped_at_creation: false,
1326 });
1327 let tangents_work = self.device.create_buffer(&wgpu::BufferDescriptor {
1328 label: Some("tw"),
1329 size: buf_size,
1330 usage: wgpu::BufferUsages::STORAGE,
1331 mapped_at_creation: false,
1332 });
1333 let adj_re_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1334 label: Some("ar"),
1335 size: buf_size,
1336 usage: wgpu::BufferUsages::STORAGE,
1337 mapped_at_creation: false,
1338 });
1339 let adj_eps_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1340 label: Some("ae"),
1341 size: buf_size,
1342 usage: wgpu::BufferUsages::STORAGE,
1343 mapped_at_creation: false,
1344 });
1345 let grad_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1346 label: Some("go"),
1347 size: grad_size,
1348 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
1349 mapped_at_creation: false,
1350 });
1351 let hvp_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1352 label: Some("ho"),
1353 size: grad_size,
1354 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
1355 mapped_at_creation: false,
1356 });
1357 let grad_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
1358 label: Some("gs"),
1359 size: grad_size,
1360 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1361 mapped_at_creation: false,
1362 });
1363 let hvp_staging = self.device.create_buffer(&wgpu::BufferDescriptor {
1364 label: Some("hs"),
1365 size: grad_size,
1366 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
1367 mapped_at_creation: false,
1368 });
1369
1370 let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
1371
1372 let io_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1373 label: Some("trev_io"),
1374 layout: &self.tangent_rev_io_bind_group_layout,
1375 entries: &[
1376 wgpu::BindGroupEntry {
1377 binding: 0,
1378 resource: primal_buf.as_entire_binding(),
1379 },
1380 wgpu::BindGroupEntry {
1381 binding: 1,
1382 resource: seed_buf.as_entire_binding(),
1383 },
1384 wgpu::BindGroupEntry {
1385 binding: 2,
1386 resource: primals_work.as_entire_binding(),
1387 },
1388 wgpu::BindGroupEntry {
1389 binding: 3,
1390 resource: tangents_work.as_entire_binding(),
1391 },
1392 wgpu::BindGroupEntry {
1393 binding: 4,
1394 resource: adj_re_buf.as_entire_binding(),
1395 },
1396 wgpu::BindGroupEntry {
1397 binding: 5,
1398 resource: adj_eps_buf.as_entire_binding(),
1399 },
1400 wgpu::BindGroupEntry {
1401 binding: 6,
1402 resource: grad_buf.as_entire_binding(),
1403 },
1404 wgpu::BindGroupEntry {
1405 binding: 7,
1406 resource: hvp_buf.as_entire_binding(),
1407 },
1408 ],
1409 });
1410
1411 let mut encoder = self
1412 .device
1413 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1414 label: Some("hvp_enc"),
1415 });
1416 {
1417 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1418 label: Some("trev_pass"),
1419 timestamp_writes: None,
1420 });
1421 pass.set_pipeline(&self.tangent_rev_pipeline);
1422 pass.set_bind_group(0, &tape_bg, &[]);
1423 pass.set_bind_group(1, &io_bg, &[]);
1424 self.check_dispatch_1d(batch_size.div_ceil(256))?;
1425 pass.dispatch_workgroups(batch_size.div_ceil(256), 1, 1);
1426 }
1427
1428 encoder.copy_buffer_to_buffer(&grad_buf, 0, &grad_staging, 0, grad_size);
1429 encoder.copy_buffer_to_buffer(&hvp_buf, 0, &hvp_staging, 0, grad_size);
1430 let sub_idx = self.queue.submit(std::iter::once(encoder.finish()));
1431
1432 let gs = grad_staging.slice(..);
1433 let hs = hvp_staging.slice(..);
1434 let (tx1, rx1) = std::sync::mpsc::channel();
1435 let (tx2, rx2) = std::sync::mpsc::channel();
1436 gs.map_async(wgpu::MapMode::Read, move |r| {
1437 let _ = tx1.send(r);
1438 });
1439 hs.map_async(wgpu::MapMode::Read, move |r| {
1440 let _ = tx2.send(r);
1441 });
1442 self.device
1447 .poll(wgpu::PollType::Wait {
1448 submission_index: Some(sub_idx),
1449 timeout: None,
1450 })
1451 .map_err(|e| GpuError::Other(format!("device poll failed: {e}")))?;
1452
1453 rx1.recv()
1454 .map_err(|e| GpuError::Other(format!("{e}")))?
1455 .map_err(|e| GpuError::Other(format!("{e}")))?;
1456 rx2.recv()
1457 .map_err(|e| GpuError::Other(format!("{e}")))?
1458 .map_err(|e| GpuError::Other(format!("{e}")))?;
1459
1460 let gd = gs.get_mapped_range();
1461 let grads: Vec<f32> = bytemuck::cast_slice(&gd).to_vec();
1462 drop(gd);
1463 grad_staging.unmap();
1464
1465 let hd = hs.get_mapped_range();
1466 let hvps: Vec<f32> = bytemuck::cast_slice(&hd).to_vec();
1467 drop(hd);
1468 hvp_staging.unmap();
1469
1470 Ok((grads, hvps))
1471 }
1472
1473 fn sparse_hessian(
1480 &self,
1481 tape: &WgpuTapeBuffers,
1482 tape_cpu: &mut crate::BytecodeTape<f32>,
1483 x: &[f32],
1484 ) -> Result<(f32, Vec<f32>, crate::sparse::SparsityPattern, Vec<f32>), GpuError> {
1485 let ni = tape.num_inputs as usize;
1486
1487 let pattern = tape_cpu.detect_sparsity();
1489 let (colors, num_colors) = crate::sparse::greedy_coloring(&pattern);
1490
1491 if num_colors == 0 {
1492 tape_cpu.forward(x);
1493 let val = tape_cpu.output_value();
1494 let grad = tape_cpu.gradient(x);
1495 return Ok((val, grad, pattern, vec![]));
1496 }
1497
1498 let batch = num_colors;
1500 let mut tangent_dirs = Vec::with_capacity(batch as usize * ni);
1501 for c in 0..num_colors {
1502 for &color in &colors[..ni] {
1503 tangent_dirs.push(if color == c { 1.0f32 } else { 0.0f32 });
1504 }
1505 }
1506
1507 let (grads, hvps) = self.hvp_batch(tape, x, &tangent_dirs, batch)?;
1508
1509 let gradient: Vec<f32> = grads[..ni].to_vec();
1511
1512 let nnz = pattern.nnz();
1514 let mut hess_values = vec![0.0f32; nnz];
1515 for (k, (&row, &col)) in pattern.rows.iter().zip(pattern.cols.iter()).enumerate() {
1516 let i = row as usize;
1517 let j = col as usize;
1518 let c = colors[j] as usize;
1520 hess_values[k] = hvps[c * ni + i];
1521 }
1522
1523 tape_cpu.forward(x);
1525 let value = tape_cpu.output_value();
1526
1527 Ok((value, gradient, pattern, hess_values))
1528 }
1529
1530 #[cfg(feature = "stde")]
1531 fn taylor_forward_kth_batch(
1532 &self,
1533 tape: &WgpuTapeBuffers,
1534 primal_inputs: &[f32],
1535 direction_seeds: &[f32],
1536 batch_size: u32,
1537 order: usize,
1538 ) -> Result<super::TaylorKthBatchResult<f32>, GpuError> {
1539 self.taylor_forward_kth_batch(tape, primal_inputs, direction_seeds, batch_size, order)
1541 }
1542
1543 }
1545
1546fn bgl_storage_ro(binding: u32) -> wgpu::BindGroupLayoutEntry {
1549 wgpu::BindGroupLayoutEntry {
1550 binding,
1551 visibility: wgpu::ShaderStages::COMPUTE,
1552 ty: wgpu::BindingType::Buffer {
1553 ty: wgpu::BufferBindingType::Storage { read_only: true },
1554 has_dynamic_offset: false,
1555 min_binding_size: None,
1556 },
1557 count: None,
1558 }
1559}
1560
1561fn bgl_storage_rw(binding: u32) -> wgpu::BindGroupLayoutEntry {
1562 wgpu::BindGroupLayoutEntry {
1563 binding,
1564 visibility: wgpu::ShaderStages::COMPUTE,
1565 ty: wgpu::BindingType::Buffer {
1566 ty: wgpu::BufferBindingType::Storage { read_only: false },
1567 has_dynamic_offset: false,
1568 min_binding_size: None,
1569 },
1570 count: None,
1571 }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 use super::*;
1577
1578 #[test]
1581 fn dispatch_1d_guard_rejects_over_limit() {
1582 let Some(ctx) = WgpuContext::new() else {
1583 return;
1584 };
1585 let max = ctx.device.limits().max_compute_workgroups_per_dimension;
1586 assert!(ctx.check_dispatch_1d(max).is_ok());
1587 assert!(ctx.check_dispatch_1d(max + 1).is_err());
1588 assert!(ctx.check_dispatch_1d(u32::MAX).is_err());
1589 }
1590}