Skip to main content

echidna/gpu/
wgpu_backend.rs

1//! wgpu compute backend for GPU-accelerated tape evaluation.
2//!
3//! Cross-platform (Metal, Vulkan, DX12). f32 only — WGSL does not support f64.
4
5use super::{GpuBackend, GpuError, GpuTapeData, TapeMeta};
6
7/// GPU buffers holding an uploaded tape (wgpu backend).
8pub 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
20/// wgpu compute context — holds the device, queue, and compiled pipelines.
21pub 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    /// K-specialized Taylor forward pipelines for K=1..5 (index = K-1).
29    #[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    /// Acquire a GPU device. Returns `None` if no suitable adapter is found.
42    #[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        // wgpu 29 tightened downlevel defaults — `max_storage_buffers_per_shader_stage`
59        // dropped. The tangent-reverse pipeline binds 13 storage buffers (5 in the
60        // tape layout + 8 in the tangent-reverse I/O layout); request that explicitly
61        // and fall back to whatever the adapter allows if it's higher.
62        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        // Bind group layout 0: tape data (read-only storage + uniform)
76        let tape_bind_group_layout =
77            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
78                label: Some("echidna_tape_bgl"),
79                entries: &[
80                    // binding 0: opcodes
81                    bgl_storage_ro(0),
82                    // binding 1: arg0
83                    bgl_storage_ro(1),
84                    // binding 2: arg1
85                    bgl_storage_ro(2),
86                    // binding 3: constants
87                    bgl_storage_ro(3),
88                    // binding 4: TapeMeta uniform
89                    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                    // binding 5: output_indices
100                    bgl_storage_ro(5),
101                ],
102            });
103
104        // Bind group layout 1a: forward I/O buffers
105        let forward_io_bind_group_layout =
106            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
107                label: Some("echidna_fwd_io_bgl"),
108                entries: &[
109                    // binding 0: inputs [B * num_inputs] (read-only)
110                    bgl_storage_ro(0),
111                    // binding 1: values [B * num_variables] (read-write)
112                    bgl_storage_rw(1),
113                    // binding 2: outputs [B * num_outputs] (read-write)
114                    bgl_storage_rw(2),
115                ],
116            });
117
118        // Bind group layout 1b: reverse I/O buffers
119        let reverse_io_bind_group_layout =
120            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
121                label: Some("echidna_rev_io_bgl"),
122                entries: &[
123                    // binding 0: values [B * num_variables] (read-only, from forward)
124                    bgl_storage_ro(0),
125                    // binding 1: adjoints [B * num_variables] (read-write)
126                    bgl_storage_rw(1),
127                    // binding 2: grad_out [B * num_inputs] (read-write)
128                    bgl_storage_rw(2),
129                ],
130            });
131
132        // Forward pipeline
133        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        // Reverse pipeline
157        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        // Tangent forward pipeline
181        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), // primal_inputs
186                    bgl_storage_ro(1), // tangent_seeds
187                    bgl_storage_rw(2), // primals working
188                    bgl_storage_rw(3), // tangents working
189                    bgl_storage_rw(4), // tangent_outputs
190                ],
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        // Tangent reverse pipeline (forward-over-reverse for HVP)
218        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), // primal_inputs
223                    bgl_storage_ro(1), // tangent_seeds
224                    bgl_storage_rw(2), // primals working
225                    bgl_storage_rw(3), // tangents working
226                    bgl_storage_rw(4), // adj_re
227                    bgl_storage_rw(5), // adj_eps
228                    bgl_storage_rw(6), // grad_out
229                    bgl_storage_rw(7), // hvp_out
230                ],
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        // Taylor forward 2nd-order pipeline (STDE only)
258        #[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), // primal_inputs
264                    bgl_storage_ro(1), // direction_seeds
265                    bgl_storage_rw(2), // jets working buffer
266                    bgl_storage_rw(3), // jet_outputs
267                ],
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        // Compile K-specialized Taylor forward pipelines for K=1..5
281        // (replaces the former handwritten taylor_forward_2nd.wgsl shader)
282        #[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    /// Create the tape bind group (group 0) shared by all dispatch methods.
323    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    /// Batched K-th order Taylor forward propagation on GPU.
361    ///
362    /// Supports `order` (K) from 1 to 5. Each batch element pushes one direction
363    /// through the tape, producing a Taylor jet with K coefficients.
364    ///
365    /// `primal_inputs` is `[f32; batch_size * num_inputs]`.
366    /// `direction_seeds` is `[f32; batch_size * num_inputs]` — only c1 seeds are used.
367    ///
368    /// Returns `TaylorKthBatchResult` with K coefficient vectors.
369    #[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        // SAFETY(u32 cast): order is validated above to be in 1..=5.
387        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        // WGSL indexes the jet buffer with `bid * nv * K` in u32; for K=4 or
393        // K=5 at realistic nv, this can overflow before the CPU-side index
394        // arithmetic catches it. Enforce the u32 envelope here so that the
395        // kernel's 32-bit indexing remains safe.
396        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        // Jets working buffer: B * nv * K floats
445        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        // Jet outputs: B * n_out * K floats
454        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        // Propagate `device.poll` failures rather than swallowing them —
522        // on device loss (driver reset, OOM in another submission) a
523        // silently-ignored poll error turned into an indefinite wait on
524        // the `rx.recv()` below, looking like a deadlock to callers.
525        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        // Deinterleave: raw is [c0, c1, ..., c_{K-1}] per output per batch element
540        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    /// Reject a 1-D dispatch whose workgroup count would exceed the device's
562    /// `max_compute_workgroups_per_dimension` limit (65535 on the wgpu
563    /// downlevel defaults), returning a clean `GpuError` instead of the opaque
564    /// wgpu validation error a too-large batch would otherwise raise.
565    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    /// Evaluate the tape at `batch_size` input points in parallel on the GPU.
654    ///
655    /// `inputs` is a flat `[f32; batch_size * num_inputs]` array in row-major order:
656    /// `[x0_0, x0_1, ..., x0_n, x1_0, x1_1, ..., x1_n, ...]`.
657    ///
658    /// Returns `[f32; batch_size * num_outputs]` — one output per batch element
659    /// (or `num_outputs` per element for multi-output tapes).
660    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        // Guard against u32 overflow in WGSL index arithmetic: batch_id * num_vars
679        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        // Create per-dispatch meta uniform with batch_size
685        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        // Input buffer (read-only from shader)
702        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        // Values buffer: B * num_variables (working memory per thread)
711        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        // Output buffer: B * num_outputs
720        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        // Staging buffer for readback
729        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        // Tape bind group (group 0)
737        let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
738
739        // I/O bind group (group 1)
740        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        // Encode and dispatch
760        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        // Readback
782        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        // Propagate `device.poll` failures rather than swallowing them —
788        // on device loss (driver reset, OOM in another submission) a
789        // silently-ignored poll error turned into an indefinite wait on
790        // the `rx.recv()` below, looking like a deadlock to callers.
791        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    /// Compute gradients at `batch_size` input points in parallel on the GPU.
811    ///
812    /// Runs forward evaluation followed by a reverse adjoint sweep.
813    ///
814    /// Returns `(outputs, gradients)`:
815    /// - `outputs`: `[f32; batch_size * num_outputs]`
816    /// - `gradients`: `[f32; batch_size * num_inputs]` in row-major order
817    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        // Guard against u32 overflow in WGSL index arithmetic: batch_id * num_vars
836        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        // Create per-dispatch meta uniform
842        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        // Input buffer
859        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        // Values buffer: B * num_variables (shared between forward and reverse)
868        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        // Output buffer: B * num_outputs
877        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        // Adjoint buffer: B * num_variables
887        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        // Gradient output buffer: B * num_inputs
895        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        // Staging buffers for readback
905        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        // Tape bind group (shared between forward and reverse)
919        let tape_bg = self.create_tape_bind_group(tape, &meta_buf);
920
921        // Forward I/O bind group
922        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        // Reverse I/O bind group (values is read-only here, from forward pass)
942        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        // Encode: forward pass → reverse pass → copy results
965        let mut encoder = self
966            .device
967            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
968                label: Some("gradient_enc"),
969            });
970
971        // Forward pass
972        {
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        // Reverse pass
984        {
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        // Readback both buffers
1000        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        // Propagate `device.poll` failures rather than swallowing them —
1013        // on device loss (driver reset, OOM in another submission) a
1014        // silently-ignored poll error turned into an indefinite wait on
1015        // the `rx.recv()` below, looking like a deadlock to callers.
1016        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    /// Compute a sparse Jacobian using forward-mode tangent sweeps on GPU.
1043    ///
1044    /// CPU performs sparsity detection and graph coloring; GPU dispatches all
1045    /// colored tangent sweeps in parallel.
1046    ///
1047    /// `tape_cpu` is needed for sparsity detection (which uses the tape structure).
1048    /// `x` is the evaluation point.
1049    ///
1050    /// Returns `(output_values, sparsity_pattern, jacobian_values)`:
1051    /// - `output_values`: function values at x
1052    /// - `sparsity_pattern`: the Jacobian sparsity pattern
1053    /// - `jacobian_values`: non-zero Jacobian entries matching the pattern
1054    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        // CPU: detect sparsity and compute coloring
1067        let pattern = tape_cpu.detect_jacobian_sparsity();
1068        let (colors, num_colors) = crate::sparse::column_coloring(&pattern);
1069
1070        if num_colors == 0 {
1071            // All-zero Jacobian
1072            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        // Guard against u32 overflow in WGSL `bid * num_variables` index
1079        // arithmetic. The effective batch_size for sparse-Jacobian dispatch
1080        // is `num_colors` (one JVP per color); if the colored tape plus the
1081        // variable count exceeds u32, indices would silently wrap.
1082        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        // Build tangent seed vectors: one per color
1088        // Each color c gets a seed where input[i].tangent = 1 if colors[i] == c, else 0
1089        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        // Create per-dispatch meta
1101        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        // Propagate `device.poll` failures rather than swallowing them —
1214        // on device loss (driver reset, OOM in another submission) a
1215        // silently-ignored poll error turned into an indefinite wait on
1216        // the `rx.recv()` below, looking like a deadlock to callers.
1217        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        // CPU: extract Jacobian entries from compressed tangent results
1233        // tangent_results[c * num_outputs + o] = sum over {i : colors[i]==c} J[o][i] * 1
1234        // Since coloring is valid, each entry J[o][i] appears in exactly one color.
1235        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; // output index
1240            let i = col as usize; // input index
1241            let c = colors[i] as usize;
1242            jac_values[k] = tangent_results[c * num_outputs + o];
1243        }
1244
1245        // Get output values from CPU (could also read from GPU primals, but simpler)
1246        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    /// Batched Hessian-vector product via forward-over-reverse on GPU.
1253    ///
1254    /// Dispatches `batch_size` HVP computations in parallel, each with the same
1255    /// primal inputs `x` but different tangent directions.
1256    ///
1257    /// `tangent_dirs` is `[f32; batch_size * num_inputs]` — one direction per element.
1258    ///
1259    /// Returns `(gradients, hvps)` each of shape `[f32; batch_size * num_inputs]`.
1260    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        // Guard against u32 overflow in WGSL index arithmetic
1276        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        // Build primal inputs: same x replicated for each batch element
1282        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        // Propagate `device.poll` failures rather than swallowing them —
1443        // on device loss (driver reset, OOM in another submission) a
1444        // silently-ignored poll error turned into an indefinite wait on
1445        // the `rx.recv()` below, looking like a deadlock to callers.
1446        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    /// Compute a sparse Hessian using forward-over-reverse HVP sweeps on GPU.
1474    ///
1475    /// CPU performs Hessian sparsity detection and distance-2 coloring; GPU
1476    /// dispatches all colored HVP sweeps in parallel.
1477    ///
1478    /// Returns `(value, gradient, sparsity_pattern, hessian_values)`.
1479    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        // CPU: detect sparsity and compute distance-2 coloring
1488        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        // Build tangent seeds: one per color
1499        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        // Extract gradient from first HVP (all share the same gradient)
1510        let gradient: Vec<f32> = grads[..ni].to_vec();
1511
1512        // Extract Hessian entries from compressed HVP results
1513        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            // H[i][j] = hvp[color_of_j][i] (since seed_j = 1 for color_of_j)
1519            let c = colors[j] as usize;
1520            hess_values[k] = hvps[c * ni + i];
1521        }
1522
1523        // Get function value from CPU
1524        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        // Delegate to the inherent method
1540        self.taylor_forward_kth_batch(tape, primal_inputs, direction_seeds, batch_size, order)
1541    }
1542
1543    // taylor_forward_2nd_batch: uses default trait impl (delegates to kth_batch(order=3))
1544}
1545
1546// ── Helpers ──
1547
1548fn 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    // Metal-gated: the 1-D dispatch guard rejects a workgroup count above the
1579    // device limit and accepts one at the limit (skips when no adapter).
1580    #[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}