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