yum/gpu_fft

A GPU-friendly FFT

git clone https://git.yummers.dev/yum/gpu_fft

yumAdd Pass constructcfe94cd

master
18.3 KiB558 linesraw
1use num::Complex;
2use std::borrow::Cow;
3use std::num::NonZeroU64;
4use std::sync::mpsc::{Receiver, Sender};
5use wgpu::naga;
6
7pub struct GpuShit {
8    adapter: wgpu::Adapter,
9    device: wgpu::Device,
10    queue: wgpu::Queue,
11}
12
13impl GpuShit {
14    pub fn get() -> anyhow::Result<GpuShit> {
15        env_logger::init();
16        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
17        let adapter =
18            pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))?;
19        println!("Got adapter: {:#?}", adapter.get_info());
20
21        let (device, queue) =
22            pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
23                label: None,
24                required_features: wgpu::Features::empty(),
25                required_limits: wgpu::Limits::downlevel_defaults(),
26                #[cfg(target_arch = "wasm32")]
27                default_queue: wgpu::QueueDescriptor { label: None },
28                experimental_features: wgpu::ExperimentalFeatures::disabled(),
29                memory_hints: wgpu::MemoryHints::MemoryUsage,
30                trace: wgpu::Trace::Off,
31            }))?;
32
33        Ok(GpuShit {
34            adapter,
35            device,
36            queue,
37        })
38    }
39}
40
41fn get_entry_point_name(module: &naga::Module) -> anyhow::Result<String> {
42    if module.entry_points.len() != 1 {
43        anyhow::bail!(
44            "Expected exactly one entry point, found {}",
45            module.entry_points.len()
46        );
47    }
48    for entry in module.entry_points.iter() {
49        return Ok(entry.name.clone());
50    }
51    anyhow::bail!("Could not find entry point");
52}
53
54fn get_entry_point_workgroup_size(module: &naga::Module, name: &str) -> anyhow::Result<[u32; 3]> {
55    for entry_point in module.entry_points.iter() {
56        if entry_point.name != name {
57            continue;
58        }
59        return Ok(entry_point.workgroup_size);
60    }
61    anyhow::bail!("Could not find entry point named {}", name);
62}
63
64fn get_bind_group_layout(
65    gpu: &GpuShit,
66    module: &naga::Module,
67    module_info: &naga::valid::ModuleInfo,
68    module_layout: &naga::proc::Layouter,
69) -> anyhow::Result<wgpu::BindGroupLayout> {
70    let mut entries = Vec::<wgpu::BindGroupLayoutEntry>::new();
71    for (global_handle, global) in module.global_variables.iter() {
72        let Some(binding) = &global.binding else {
73            continue;
74        };
75
76        let can_write = match global.space {
77            naga::AddressSpace::Storage { access } => {
78                access.contains(naga::StorageAccess::STORE)
79                    || access.contains(naga::StorageAccess::ATOMIC)
80            }
81            _ => {
82                eprintln!(
83                    "Skipping global {:?} with address space {:?}",
84                    global.name, global.space
85                );
86                continue;
87            }
88        };
89
90        let mut stages = wgpu::ShaderStages::empty();
91        for (entry_idx, entry) in module.entry_points.iter().enumerate() {
92            let usage = module_info.get_entry_point(entry_idx)[global_handle];
93            if usage.is_empty() {
94                continue;
95            }
96
97            stages |= match entry.stage {
98                naga::ShaderStage::Vertex => wgpu::ShaderStages::VERTEX,
99                naga::ShaderStage::Fragment => wgpu::ShaderStages::FRAGMENT,
100                naga::ShaderStage::Compute => wgpu::ShaderStages::COMPUTE,
101                _ => continue,
102            }
103        }
104        if stages.is_empty() {
105            eprintln!(
106                "Skipping bound global {:?} which is not referenced by any stage",
107                global.name
108            );
109            continue;
110        }
111
112        let global_ty = &module.types[global.ty];
113        let entry = match module.types[global.ty].inner {
114            naga::ir::TypeInner::Array { .. } => {
115                let min_size = module_layout[global.ty].size;
116                wgpu::BindGroupLayoutEntry {
117                    binding: binding.binding,
118                    visibility: stages,
119                    ty: wgpu::BindingType::Buffer {
120                        ty: wgpu::BufferBindingType::Storage {
121                            read_only: !can_write,
122                        },
123                        min_binding_size: NonZeroU64::new(min_size as u64),
124                        has_dynamic_offset: false,
125                    },
126                    count: None,
127                }
128            }
129            _ => {
130                anyhow::bail!(format!(
131                    "Unsupported global type: {:?} (name={:?})",
132                    global_ty.name, global_ty.inner
133                ))
134            }
135        };
136        entries.push(entry);
137    }
138    let layout = wgpu::BindGroupLayoutDescriptor {
139        label: None,
140        entries: &entries,
141    };
142    Ok(gpu.device.create_bind_group_layout(&layout))
143}
144
145pub struct ShaderShit {
146    bind_group_layout: wgpu::BindGroupLayout,
147    shader_module: wgpu::ShaderModule,
148    entry_point_name: String,
149    entry_point_workgroup_size: [u32; 3],
150}
151
152impl ShaderShit {
153    pub fn new(gpu: &GpuShit, shader: &str) -> anyhow::Result<Self> {
154        let module = naga::front::wgsl::parse_str(shader)?;
155        let module_info = naga::valid::Validator::new(
156            naga::valid::ValidationFlags::all(),
157            naga::valid::Capabilities::all(),
158        )
159        .validate(&module)?;
160        let mut module_layout = naga::proc::Layouter::default();
161        module_layout.update(module.to_ctx())?;
162
163        Self::ensure_compute_compatibility(gpu, &module)?;
164        let bind_group_layout = get_bind_group_layout(gpu, &module, &module_info, &module_layout)?;
165        let entry_point_name = get_entry_point_name(&module)?;
166        let entry_point_workgroup_size =
167            get_entry_point_workgroup_size(&module, &entry_point_name)?;
168
169        let module_descriptor = wgpu::ShaderModuleDescriptor {
170            label: None,
171            source: wgpu::ShaderSource::Naga(Cow::Owned(module)),
172        };
173        let shader_module = gpu.device.create_shader_module(module_descriptor);
174
175        Ok(Self {
176            bind_group_layout,
177            shader_module,
178            entry_point_name,
179            entry_point_workgroup_size,
180        })
181    }
182
183    fn ensure_compute_compatibility(gpu: &GpuShit, module: &naga::Module) -> anyhow::Result<()> {
184        let mut has_compute_stage = false;
185        for entry_point in module.entry_points.iter() {
186            if entry_point.stage == naga::ShaderStage::Compute {
187                has_compute_stage = true;
188            }
189        }
190        if !has_compute_stage {
191            return Ok(());
192        }
193        let downlevel_caps = gpu.adapter.get_downlevel_capabilities();
194        if !downlevel_caps
195            .flags
196            .contains(wgpu::DownlevelFlags::COMPUTE_SHADERS)
197        {
198            anyhow::bail!(
199                "GPU {} does not support compute shaders.",
200                gpu.adapter.get_info().name
201            )
202        }
203        Ok(())
204    }
205}
206
207#[derive(Clone)]
208pub struct MaterialPass {
209    buffers: Vec<wgpu::Buffer>,
210    bind_group: wgpu::BindGroup,
211}
212
213impl MaterialPass {
214    pub fn new(gpu: &GpuShit, shader: &ShaderShit, data: &[Complex<f32>]) -> Self {
215        let input_data_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
216            label: None,
217            size: std::mem::size_of_val(data) as u64,
218            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
219            mapped_at_creation: false,
220        });
221        let output_data_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
222            label: None,
223            size: input_data_buffer.size(),
224            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
225            mapped_at_creation: false,
226        });
227        let download_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
228            label: None,
229            size: input_data_buffer.size(),
230            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
231            mapped_at_creation: false,
232        });
233        let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
234            label: None,
235            layout: &shader.bind_group_layout,
236            entries: &[
237                wgpu::BindGroupEntry {
238                    binding: 0,
239                    resource: input_data_buffer.as_entire_binding(),
240                },
241                wgpu::BindGroupEntry {
242                    binding: 1,
243                    resource: output_data_buffer.as_entire_binding(),
244                },
245            ],
246        });
247        Self {
248            buffers: vec![input_data_buffer, output_data_buffer, download_buffer],
249            bind_group: bind_group,
250        }
251    }
252
253    pub fn from_buffers(gpu: &GpuShit, shader: &ShaderShit, buffers: Vec<wgpu::Buffer>) -> Self {
254        let descriptor_entries =
255            buffers
256                .iter()
257                .enumerate()
258                .fold(Vec::new(), |mut entries, (index, buf)| {
259                    entries.push(wgpu::BindGroupEntry {
260                        binding: index as u32,
261                        resource: buf.as_entire_binding(),
262                    });
263                    entries
264                });
265        let descriptor_seed = wgpu::BindGroupDescriptor {
266            label: None,
267            layout: &shader.bind_group_layout,
268            entries: &descriptor_entries,
269        };
270        let bind_group = gpu.device.create_bind_group(&descriptor_seed);
271        Self {
272            buffers: buffers,
273            bind_group: bind_group,
274        }
275    }
276}
277
278struct MaterialInstance {
279    job_id: u64,
280    busy: bool,
281    passes: Vec<MaterialPass>,
282}
283
284impl MaterialInstance {
285    pub fn new(passes: Vec<MaterialPass>) -> Self {
286        Self {
287            job_id: 0, // Placeholder. Job ID is assigned when data is submitted.
288            busy: false,
289            passes: passes,
290        }
291    }
292}
293
294struct MaterialShit {
295    slots: Vec<MaterialInstance>,
296    next_job_id: u64,
297    ready_tx: Sender<(usize, u64, Result<(), wgpu::BufferAsyncError>)>,
298    ready_rx: Receiver<(usize, u64, Result<(), wgpu::BufferAsyncError>)>,
299}
300
301impl MaterialShit {
302    #[allow(dead_code)]
303    pub fn new(gpu: &GpuShit, shader: &ShaderShit, data: &[Complex<f32>], n_slots: usize) -> Self {
304        let mut slots = Vec::<MaterialInstance>::new();
305        for _ in 0..n_slots {
306            let pass = MaterialPass::new(gpu, shader, data);
307            slots.push(MaterialInstance::new(vec![pass.clone()]));
308        }
309        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
310        Self {
311            slots: slots,
312            next_job_id: 0,
313            ready_tx: ready_tx,
314            ready_rx: ready_rx,
315        }
316    }
317
318    pub fn get_instance(&mut self) -> Option<(&mut MaterialInstance, usize, u64)> {
319        let (slot_idx, slot) = self
320            .slots
321            .iter_mut()
322            .enumerate()
323            .find(|(_, slot)| !slot.busy)?;
324        let job_id = self.next_job_id;
325        slot.busy = true;
326        slot.job_id = self.next_job_id;
327        self.next_job_id += 1;
328        Some((slot, slot_idx, job_id))
329    }
330}
331
332#[allow(dead_code)]
333fn get_pipeline(gpu: &GpuShit, shader: &ShaderShit) -> wgpu::ComputePipeline {
334    let pipeline_layout = gpu
335        .device
336        .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
337            label: None,
338            bind_group_layouts: &[Some(&shader.bind_group_layout)],
339            immediate_size: 0,
340        });
341
342    gpu.device
343        .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
344            label: None,
345            layout: Some(&pipeline_layout),
346            module: &shader.shader_module,
347            entry_point: Some(&shader.entry_point_name),
348            compilation_options: wgpu::PipelineCompilationOptions::default(),
349            cache: None,
350        })
351}
352
353#[allow(dead_code)]
354fn submit_dft_batch(
355    gpu: &GpuShit,
356    shader: &ShaderShit,
357    material: &mut MaterialShit,
358    pipeline: &wgpu::ComputePipeline,
359    data_batch: &[Vec<Complex<f32>>],
360) -> anyhow::Result<usize> {
361    // Submit data to the GPU for each input in the batch.
362    let mut jobs = Vec::with_capacity(data_batch.len());
363    for data in data_batch {
364        let Some((slot, slot_idx, job_id)) = material.get_instance() else {
365            break;
366        };
367        gpu.queue
368            .write_buffer(&slot.passes[0].buffers[0], 0, bytemuck::cast_slice(data));
369        jobs.push((slot_idx, job_id, data.len()));
370    }
371    if jobs.is_empty() {
372        return Ok(0);
373    }
374
375    let mut encoder = gpu
376        .device
377        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
378    let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
379        label: None,
380        timestamp_writes: None,
381    });
382    compute_pass.set_pipeline(pipeline);
383    let shader_local_size = shader
384        .entry_point_workgroup_size
385        .iter()
386        .fold(1, |acc, &x| acc * x) as usize;
387    for &(slot_idx, _, len) in &jobs {
388        compute_pass.set_bind_group(0, &material.slots[slot_idx].passes[0].bind_group, &[]);
389        compute_pass.dispatch_workgroups(len.div_ceil(shader_local_size) as u32, 1, 1);
390    }
391    drop(compute_pass);
392
393    for &(slot_idx, job_id, _) in &jobs {
394        let slot = &material.slots[slot_idx];
395        encoder.copy_buffer_to_buffer(
396            &slot.passes[0].buffers[1],
397            0,
398            &slot.passes[0].buffers[2],
399            0,
400            slot.passes[0].buffers[1].size(),
401        );
402        let ready_tx = material.ready_tx.clone();
403        encoder.map_buffer_on_submit(
404            &slot.passes[0].buffers[2],
405            wgpu::MapMode::Read,
406            ..,
407            move |result| {
408                ready_tx.send((slot_idx, job_id, result)).ok();
409            },
410        );
411    }
412
413    gpu.queue.submit([encoder.finish()]);
414    Ok(jobs.len())
415}
416
417#[allow(dead_code)]
418fn drain_dft(
419    gpu: &GpuShit,
420    material: &mut MaterialShit,
421) -> anyhow::Result<Vec<(u64, Vec<Complex<f32>>)>> {
422    gpu.device.poll(wgpu::PollType::Poll)?;
423    let mut results = Vec::new();
424    while let Ok((slot_index, job_id, map_result)) = material.ready_rx.try_recv() {
425        let slot = &mut material.slots[slot_index];
426        let data = slot.passes[0].buffers[2].get_mapped_range(..)?;
427        let result = bytemuck::cast_slice(&data).to_vec();
428        drop(data);
429
430        slot.passes[0].buffers[2].unmap();
431        slot.busy = false;
432
433        map_result?;
434        results.push((job_id, result));
435    }
436    Ok(results)
437}
438
439#[cfg(test)]
440mod tests {
441    use std::time::Duration;
442    use std::time::Instant;
443
444    use num::Complex;
445    use num::traits::Float;
446    use rand::RngExt;
447    use rand::SeedableRng;
448    use rand::rngs::StdRng;
449
450    // Cast one float type to another, truncating if needed.
451    fn t0_to_t1<T0: Float, T1: Float>(val: T0) -> T1 {
452        return num::cast(val).unwrap();
453    }
454
455    // Casts one Complex<T> to another.
456    fn complex_to_t<T0: Float, T1: Float>(data: &[Complex<T0>]) -> Vec<Complex<T1>> {
457        data.iter()
458            .map(|c| Complex::new(t0_to_t1(c.re), t0_to_t1(c.im)))
459            .collect()
460    }
461
462    fn evaluate_results(
463        result_ref_64: &Vec<Complex<f64>>,
464        result_cur_64: &Vec<Complex<f64>>,
465        duration_dft: std::time::Duration,
466        duration_cur: std::time::Duration,
467        algo_name: &str,
468    ) {
469        assert_eq!(result_ref_64.len(), result_cur_64.len());
470
471        println!("  Algorithm:  {}", algo_name);
472        println!("    Duration:  {:?}", duration_cur);
473        println!(
474            "    Speedup:   {:?}",
475            duration_dft.as_nanos() as f32 / duration_cur.as_nanos() as f32
476        );
477
478        let mut max_err: f64 = 0.0;
479        let mut sum_err: f64 = 0.0;
480        // TODO: median, 90p, 99p
481
482        let result_64: Vec<Complex<f64>> = complex_to_t(&result_cur_64);
483
484        for i in 0..result_ref_64.len() {
485            max_err = max_err.max((result_ref_64[i] - result_64[i]).norm());
486            sum_err += (result_ref_64[i] - result_64[i]).norm();
487        }
488        println!("    Max err:   {}", max_err);
489        println!("    Avg err:   {}", sum_err / (result_ref_64.len() as f64));
490    }
491
492    fn evaluate_naive_dft(data: &[Complex<f32>], gpu: &super::GpuShit) {
493        let shader =
494            super::ShaderShit::new(&gpu, include_str!("generated/shaders/naive_dft.wgsl")).unwrap();
495        let mut result_dft = Vec::from(data);
496
497        const BATCH_SIZE: usize = 64;
498        let inputs = vec![Vec::from(data); BATCH_SIZE];
499        let mut material = super::MaterialShit::new(&gpu, &shader, &result_dft, BATCH_SIZE * 2);
500
501        let pipeline = super::get_pipeline(&gpu, &shader);
502
503        let warmup = Instant::now() + Duration::from_secs(3);
504        let end = warmup + Duration::from_secs(5);
505        let mut n_iter: u32 = 0;
506        let mut now = Instant::now();
507        while now < end {
508            // Fill the pipeline.
509            if super::submit_dft_batch(&gpu, &shader, &mut material, &pipeline, &inputs).unwrap()
510                > 0
511            {
512                continue;
513            }
514            // Drain if pipeline is full.
515            let results = super::drain_dft(&gpu, &mut material).unwrap();
516            for (_, result) in &results {
517                result_dft = result.clone();
518            }
519            if now > warmup {
520                n_iter += results.len() as u32;
521            }
522            now = Instant::now();
523        }
524        let duration_dft = (now - warmup) / n_iter;
525        println!("Duration: {:?}", duration_dft);
526
527        let mut result_ref: Vec<rustfft::num_complex::Complex<f64>> = complex_to_t(&data);
528        let mut ref_planner = rustfft::FftPlanner::<f64>::new();
529        let ref_fft = ref_planner.plan_fft_forward(result_ref.len());
530        ref_fft.process(&mut result_ref);
531        let result_ref_64: Vec<Complex<f64>> = complex_to_t(&result_ref);
532
533        let result_dft_64 = complex_to_t::<f32, f64>(&result_dft);
534        evaluate_results(
535            &result_ref_64,
536            &result_dft_64,
537            duration_dft,
538            duration_dft,
539            "Naive DFT",
540        );
541    }
542
543    #[test]
544    fn run_it() {
545        let mut data_f64: Vec<Complex<f64>> = Vec::new();
546        data_f64.resize((2 as usize).pow(12), Complex::ZERO);
547        // Randomize data.
548        let mut rng = StdRng::seed_from_u64(42);
549        for i in 0..data_f64.len() {
550            data_f64[i] = Complex::new(rng.random(), rng.random());
551        }
552        let data_f32: Vec<Complex<f32>> = complex_to_t::<f64, f32>(&data_f64);
553
554        let gpu = super::GpuShit::get().unwrap();
555
556        evaluate_naive_dft(&data_f32, &gpu);
557    }
558}