Skip to main content

qualia_core_db/platform/
gpu.rs

1//! GPU integration for calculus modality.
2//!
3//! Provides cross-platform GPU compute shader execution for numerical integration
4//! and differential equation solving via portable `wgpu` (Vulkan / DX12 / Metal /
5//! WebGPU). The engine is **vendor-neutral**: there is no CUDA/cuFile path. The
6//! former NVIDIA GPUDirect-Storage bridge was removed; heterogeneous dispatch,
7//! mixed-precision policy, and the vendor-neutral storage path (mmap + staging
8//! upload) live in [`super::hetero_dispatch`].
9//!
10//! ## Architecture
11//!
12//! - **WebGPU (`wgpu`)**: portable compute on every backend; the single GPU path.
13//! - **Storage**: NVMe → OS page cache (`mmap`, `super::host`) → GPU staging upload.
14//!   (NVIDIA GDS's true NVMe→VRAM DMA was deliberately declined — vendor-lock off
15//!   the affordability critical path; see `hetero_dispatch.rs`.)
16//! - **State Tracking**: GPU results packed into Quin metadata field.
17
18use crate::NQuin;
19use std::io::Seek;
20use std::path::Path;
21use wgpu::util::DeviceExt;
22
23/// The platform GPU integrator — uniformly the portable `wgpu` integrator on every
24/// backend. (The former CUDA/cuFile `CudaIntegrator` alias was removed for vendor
25/// neutrality; heterogeneous routing/fallback now lives in `super::hetero_dispatch`.)
26pub use WebGpuIntegrator as PlatformGpuIntegrator;
27
28// ─── Errors ─────────────────────────────────────────────────────────────────────
29
30#[derive(Debug)]
31pub enum GpuError {
32    DirectStorageUnavailable(String),
33    WebGPUUnavailable(String),
34    ShaderCompilationFailed(String),
35    BufferAllocationFailed(String),
36    DispatchFailed(String),
37    ReadbackFailed(String),
38    InvalidOffset { offset: u64, required: u64 },
39}
40
41// ─── GPU Integration Trait ─────────────────────────────────────────────────────
42
43/// Platform-agnostic GPU integration interface.
44///
45/// Abstracts over DirectStorage, GPUDirect, and WebGPU to provide a unified
46/// API for GPU-accelerated calculus operations.
47pub trait GpuIntegrator: Send {
48    /// Executes Simpson's rule integration on the GPU.
49    ///
50    /// # Arguments
51    ///
52    /// * `file_path` - Path to the data file (NVMe storage)
53    /// * `offset` - Byte offset in file (must be 4096-byte aligned)
54    /// * `size` - Number of bytes to process (must be 4096-byte aligned)
55    /// * `step_size` - Integration step size (f32 for Quin packing)
56    ///
57    /// # Returns
58    ///
59    /// The integrated value as f64, ready to pack into Quin metadata.
60    fn integrate_simpsons_gpu(
61        &mut self,
62        file_path: &Path,
63        offset: u64,
64        size: u64,
65        step_size: f32,
66    ) -> Result<f64, GpuError>;
67
68    /// Executes composite Simpson 3/8 quadrature on the GPU.
69    fn integrate_simpson_38_gpu(
70        &mut self,
71        file_path: &Path,
72        offset: u64,
73        size: u64,
74        step_size: f32,
75    ) -> Result<f64, GpuError>;
76
77    /// Returns available VRAM in bytes.
78    fn available_vram(&self) -> u64;
79}
80
81// ─── WebGPU Fallback Implementation ─────────────────────────────────────────────
82
83/// WebGPU-based GPU integrator (cross-platform fallback).
84///
85/// Uses wgpu to execute compute shaders when DirectStorage or GPUDirect
86/// are unavailable. Data flows: NVMe → CPU RAM → GPU VRAM.
87pub struct WebGpuIntegrator {
88    device: wgpu::Device,
89    queue: wgpu::Queue,
90    compute_pipeline: wgpu::ComputePipeline,
91    simpson_38_pipeline: wgpu::ComputePipeline,
92}
93
94impl WebGpuIntegrator {
95    /// Creates a new WebGPU integrator.
96    ///
97    /// Initializes wgpu device, queue, and compiles the calculus compute shader.
98    pub async fn new() -> Result<Self, GpuError> {
99        let instance = wgpu::Instance::default();
100
101        let adapter = instance
102            .request_adapter(&wgpu::RequestAdapterOptions {
103                power_preference: wgpu::PowerPreference::HighPerformance,
104                force_fallback_adapter: false,
105                compatible_surface: None,
106                // wgpu 30 added `apply_limit_buckets`; take the crate default.
107                ..Default::default()
108            })
109            .await
110            .map_err(|e| GpuError::WebGPUUnavailable(format!("No adapter found: {e}")))?;
111
112        let (device, queue) = adapter
113            .request_device(&wgpu::DeviceDescriptor::default())
114            .await
115            .map_err(|e| GpuError::WebGPUUnavailable(format!("Device request failed: {e}")))?;
116
117        // Load pre-compiled compute shader (AOT compilation)
118        let shader_src = include_str!("../shaders/calculus.wgsl");
119        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
120            label: Some("Calculus Shader"),
121            source: wgpu::ShaderSource::Wgsl(shader_src.into()),
122        });
123
124        let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
125            label: Some("Calculus Pipeline"),
126            layout: None,
127            module: &shader,
128            entry_point: Some("simpsons_integration"),
129            compilation_options: Default::default(),
130            cache: None,
131        });
132
133        let simpson_38_pipeline =
134            device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
135                label: Some("Simpson 3/8 Pipeline"),
136                layout: None,
137                module: &shader,
138                entry_point: Some("simpson_38_integration"),
139                compilation_options: Default::default(),
140                cache: None,
141            });
142
143        Ok(Self {
144            device,
145            queue,
146            compute_pipeline,
147            simpson_38_pipeline,
148        })
149    }
150
151    /// Executes a compute shader with the given input data.
152    async fn execute_compute(&self, input_data: &[u8], step_size: f32) -> Result<f64, GpuError> {
153        if input_data.len() % core::mem::size_of::<f32>() != 0 {
154            return Err(GpuError::DispatchFailed(
155                "calculus input must contain whole f32 values".to_string(),
156            ));
157        }
158        let element_count = input_data.len() / core::mem::size_of::<f32>();
159        if element_count < 3 || element_count % 2 == 0 {
160            return Err(GpuError::DispatchFailed(
161                "Simpson 1/3 requires an odd sample count of at least three".to_string(),
162            ));
163        }
164        if !step_size.is_finite() || step_size == 0.0 {
165            return Err(GpuError::DispatchFailed(
166                "step size must be finite and non-zero".to_string(),
167            ));
168        }
169
170        // Create storage buffer for input data
171        let input_buffer = self
172            .device
173            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
174                label: Some("Input Buffer"),
175                contents: input_data,
176                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
177            });
178
179        // Create workgroup reduction buffer (one f32 per workgroup)
180        // For 5000 elements with 64-thread workgroups: ceil(5000/64) = 79 workgroups
181        let num_workgroups = (element_count + 63) / 64;
182        let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
183            label: Some("Workgroup Reduction Buffer"),
184            size: (num_workgroups * 4) as u64,
185            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
186            mapped_at_creation: false,
187        });
188
189        // Create uniform buffer for step size and total element count
190        #[repr(C)]
191        #[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone)]
192        struct Uniforms {
193            step_size: f32,
194            total_elements: u32,
195        }
196
197        let uniforms = Uniforms {
198            step_size,
199            total_elements: element_count as u32,
200        };
201
202        let step_buffer = self
203            .device
204            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
205                label: Some("Uniforms Buffer"),
206                contents: bytemuck::cast_slice(&[uniforms]),
207                usage: wgpu::BufferUsages::UNIFORM,
208            });
209
210        // Create bind group
211        let bind_group_layout = self.compute_pipeline.get_bind_group_layout(0);
212        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
213            label: Some("Calculus Bind Group"),
214            layout: &bind_group_layout,
215            entries: &[
216                wgpu::BindGroupEntry {
217                    binding: 0,
218                    resource: input_buffer.as_entire_binding(),
219                },
220                wgpu::BindGroupEntry {
221                    binding: 1,
222                    resource: output_buffer.as_entire_binding(),
223                },
224                wgpu::BindGroupEntry {
225                    binding: 2,
226                    resource: step_buffer.as_entire_binding(),
227                },
228            ],
229        });
230
231        // Create command encoder
232        let mut encoder = self
233            .device
234            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
235                label: Some("Calculus Encoder"),
236            });
237
238        // Dispatch compute shader
239        {
240            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
241                label: Some("Calculus Pass"),
242                timestamp_writes: None,
243            });
244            compute_pass.set_pipeline(&self.compute_pipeline);
245            compute_pass.set_bind_group(0, &bind_group, &[]);
246            compute_pass.dispatch_workgroups(num_workgroups as u32, 1, 1);
247        }
248
249        // Copy output to staging buffer for readback
250        let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
251            label: Some("Staging Buffer"),
252            size: (num_workgroups * 4) as u64,
253            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
254            mapped_at_creation: false,
255        });
256
257        encoder.copy_buffer_to_buffer(
258            &output_buffer,
259            0,
260            &staging_buffer,
261            0,
262            (num_workgroups * 4) as u64,
263        );
264
265        // Submit commands
266        self.queue.submit(Some(encoder.finish()));
267
268        // Read back result
269        let buffer_slice = staging_buffer.slice(..);
270        let (sender, receiver) = futures_channel::oneshot::channel();
271        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
272            let _ = sender.send(result);
273        });
274
275        self.queue.submit(None);
276
277        let _mapping_result = receiver.await.unwrap();
278
279        // Get mapped slice
280        let result_data = buffer_slice
281            .get_mapped_range()
282            .expect("wgpu buffer map_range failed");
283
284        // Read workgroup results and sum using Kahan summation for precision
285        let workgroup_results: &[f32] = bytemuck::cast_slice(&*result_data);
286        let mut sum = 0.0f64;
287        let mut compensation = 0.0f64;
288
289        for &partial in workgroup_results {
290            let y = partial as f64 - compensation;
291            let t = sum + y;
292            compensation = (t - sum) - y;
293            sum = t;
294        }
295
296        Ok(sum)
297    }
298
299    /// Composite Simpson 3/8 variant of the reduction pipeline.
300    async fn execute_simpson_38_compute(
301        &self,
302        input_data: &[u8],
303        step_size: f32,
304    ) -> Result<f64, GpuError> {
305        if input_data.len() % core::mem::size_of::<f32>() != 0 {
306            return Err(GpuError::DispatchFailed(
307                "calculus input must contain whole f32 values".to_string(),
308            ));
309        }
310        let element_count = input_data.len() / core::mem::size_of::<f32>();
311        if element_count < 4 || (element_count - 1) % 3 != 0 {
312            return Err(GpuError::DispatchFailed(
313                "Simpson 3/8 requires a positive panel count divisible by three".to_string(),
314            ));
315        }
316        if !step_size.is_finite() || step_size == 0.0 {
317            return Err(GpuError::DispatchFailed(
318                "step size must be finite and non-zero".to_string(),
319            ));
320        }
321
322        let input_buffer = self
323            .device
324            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
325                label: Some("Simpson 3/8 Input Buffer"),
326                contents: input_data,
327                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
328            });
329
330        let num_workgroups = (element_count + 63) / 64;
331        let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
332            label: Some("Simpson 3/8 Reduction Buffer"),
333            size: (num_workgroups * 4) as u64,
334            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
335            mapped_at_creation: false,
336        });
337
338        #[repr(C)]
339        #[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone)]
340        struct Uniforms {
341            step_size: f32,
342            total_elements: u32,
343        }
344
345        let uniforms = Uniforms {
346            step_size,
347            total_elements: element_count as u32,
348        };
349        let step_buffer = self
350            .device
351            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
352                label: Some("Simpson 3/8 Uniforms"),
353                contents: bytemuck::cast_slice(&[uniforms]),
354                usage: wgpu::BufferUsages::UNIFORM,
355            });
356
357        let bgl = self.simpson_38_pipeline.get_bind_group_layout(0);
358        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
359            label: Some("Simpson 3/8 Bind Group"),
360            layout: &bgl,
361            entries: &[
362                wgpu::BindGroupEntry {
363                    binding: 0,
364                    resource: input_buffer.as_entire_binding(),
365                },
366                wgpu::BindGroupEntry {
367                    binding: 1,
368                    resource: output_buffer.as_entire_binding(),
369                },
370                wgpu::BindGroupEntry {
371                    binding: 2,
372                    resource: step_buffer.as_entire_binding(),
373                },
374            ],
375        });
376
377        let mut encoder = self
378            .device
379            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
380                label: Some("Simpson 3/8 Encoder"),
381            });
382        {
383            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
384                label: Some("Simpson 3/8 Pass"),
385                timestamp_writes: None,
386            });
387            pass.set_pipeline(&self.simpson_38_pipeline);
388            pass.set_bind_group(0, &bind_group, &[]);
389            pass.dispatch_workgroups(num_workgroups as u32, 1, 1);
390        }
391
392        let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
393            label: Some("Simpson 3/8 Staging"),
394            size: (num_workgroups * 4) as u64,
395            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
396            mapped_at_creation: false,
397        });
398        encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging, 0, (num_workgroups * 4) as u64);
399        self.queue.submit(Some(encoder.finish()));
400
401        let slice = staging.slice(..);
402        let (tx, rx) = futures_channel::oneshot::channel();
403        slice.map_async(wgpu::MapMode::Read, move |r| {
404            let _ = tx.send(r);
405        });
406        self.queue.submit(None);
407        let _ = rx.await.unwrap();
408
409        let mapped = slice
410            .get_mapped_range()
411            .expect("wgpu buffer map_range failed");
412        let partials: &[f32] = bytemuck::cast_slice(&*mapped);
413
414        // Kahan summation for numerical stability
415        let mut sum = 0.0f64;
416        let mut comp = 0.0f64;
417        for &p in partials {
418            let y = p as f64 - comp;
419            let t = sum + y;
420            comp = (t - sum) - y;
421            sum = t;
422        }
423        Ok(sum)
424    }
425}
426
427#[async_trait::async_trait]
428impl GpuIntegrator for WebGpuIntegrator {
429    fn integrate_simpsons_gpu(
430        &mut self,
431        file_path: &Path,
432        offset: u64,
433        size: u64,
434        step_size: f32,
435    ) -> Result<f64, GpuError> {
436        // Read data from file (CPU RAM path - fallback)
437        let mut file = std::fs::File::open(file_path)
438            .map_err(|e| GpuError::WebGPUUnavailable(format!("File open failed: {e}")))?;
439
440        use std::io::Read;
441        file.seek(std::io::SeekFrom::Start(offset))
442            .map_err(|e| GpuError::WebGPUUnavailable(format!("Seek failed: {e}")))?;
443
444        let mut buffer = vec![0u8; size as usize];
445        file.read_exact(&mut buffer)
446            .map_err(|e| GpuError::WebGPUUnavailable(format!("Read failed: {e}")))?;
447
448        // Execute on GPU
449        // Note: This is a blocking call in an async context - in production,
450        // we'd use a thread pool or async executor
451        let result = self.execute_compute(&buffer, step_size);
452
453        // For now, we'll block on the async result using the current runtime
454        // In production, this should be properly integrated with the async runtime
455        let handle = tokio::runtime::Handle::try_current()
456            .map_err(|e| GpuError::WebGPUUnavailable(format!("Tokio handle failed: {e}")))?;
457        handle.block_on(result)
458    }
459
460    fn integrate_simpson_38_gpu(
461        &mut self,
462        file_path: &Path,
463        offset: u64,
464        size: u64,
465        step_size: f32,
466    ) -> Result<f64, GpuError> {
467        // Read data from file (CPU RAM path)
468        let mut file = std::fs::File::open(file_path)
469            .map_err(|e| GpuError::WebGPUUnavailable(format!("File open failed: {e}")))?;
470
471        use std::io::Read;
472        file.seek(std::io::SeekFrom::Start(offset))
473            .map_err(|e| GpuError::WebGPUUnavailable(format!("Seek failed: {e}")))?;
474
475        let mut buffer = vec![0u8; size as usize];
476        file.read_exact(&mut buffer)
477            .map_err(|e| GpuError::WebGPUUnavailable(format!("Read failed: {e}")))?;
478
479        // Dispatch the composite Simpson 3/8 compute shader.
480        let handle = tokio::runtime::Handle::try_current()
481            .map_err(|e| GpuError::WebGPUUnavailable(format!("Tokio handle failed: {e}")))?;
482        handle.block_on(self.execute_simpson_38_compute(&buffer, step_size))
483    }
484
485    fn available_vram(&self) -> u64 {
486        // Use max_buffer_size from device limits as a practical proxy for available VRAM.
487        // On discrete GPUs this cap is typically set to the full VRAM size; on integrated
488        // GPUs it reflects the shared system memory window.
489        self.device.limits().max_buffer_size
490    }
491}
492
493// ─── GPU State Tracking ─────────────────────────────────────────────────────────
494
495/// Packs GPU computation result into Quin metadata field.
496///
497/// When the GPU finishes processing, the scalar result is written back
498/// to the host and packed into the Quin's metadata field for SLG VM resumption.
499pub fn pack_gpu_result_into_quin(quin: &mut NQuin, result: f64) {
500    quin.metadata = f64::to_bits(result);
501}
502
503/// Extracts GPU computation result from Quin metadata field.
504pub fn extract_gpu_result_from_quin(quin: &NQuin) -> f64 {
505    f64::from_bits(quin.metadata)
506}
507
508/// Creates a suspended Quin for GPU computation.
509///
510/// Packs the computation parameters into the Quin fields so the SLG VM
511/// can track the in-flight GPU operation.
512pub fn create_gpu_job_quin(job_id: u64, opcode: u8, file_offset: u64, step_size: f32) -> NQuin {
513    let mut quin = NQuin::default();
514    quin.subject = job_id;
515    quin.predicate = (opcode as u64) | (q_hash("calculus:gpu") << 8);
516    quin.object = file_offset;
517    quin.context = f32::to_bits(step_size) as u64;
518    quin.metadata = 0; // Will be filled with result
519    quin.parity = 0; // Will be computed
520    quin
521}
522
523// ─── Helper Functions ───────────────────────────────────────────────────────────
524
525/// Compile-time hashing function (reused from lib.rs) — 60-bit identity, in
526/// lockstep with `crate::q_hash` so the "calculus:gpu" predicate path joins.
527const fn q_hash(s: &str) -> u64 {
528    let mut hash: u64 = 0xcbf29ce484222325;
529    let bytes = s.as_bytes();
530    let mut i = 0;
531    while i < bytes.len() {
532        hash = hash ^ (bytes[i] as u64);
533        hash = hash.wrapping_mul(0x100000001b3);
534        i += 1;
535    }
536    hash & 0x0FFF_FFFF_FFFF_FFFF
537}
538
539// ─── Tests ─────────────────────────────────────────────────────────────────────
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_gpu_result_packing() {
547        let result = 42.5f64;
548        let mut quin = NQuin::default();
549
550        pack_gpu_result_into_quin(&mut quin, result);
551
552        let extracted = extract_gpu_result_from_quin(&quin);
553        assert_eq!(extracted, result);
554    }
555
556    #[test]
557    fn test_gpu_job_quin_creation() {
558        let quin = create_gpu_job_quin(
559            12345, 0x50, // OP_SIMPSONS_INTEGRATION
560            4096, 0.001,
561        );
562
563        assert_eq!(quin.subject, 12345);
564        assert_eq!(quin.object, 4096);
565        assert_eq!(f32::from_bits(quin.context as u32), 0.001);
566    }
567
568    #[cfg(feature = "wgsl-forge")]
569    #[test]
570    fn calculus_shader_is_portable_and_semantically_named() {
571        let source = include_str!("../shaders/calculus.wgsl");
572        crate::wgsl_forge::validate_wgsl(source).expect("calculus WGSL must Naga-validate");
573        assert!(!source.contains("array<f64>"));
574        assert!(!source.contains(": f64"));
575        assert!(!source.contains("rk4_step"));
576        assert!(source.contains("simpson_38_integration"));
577    }
578}