1pub mod wgsl;
19
20use std::fmt;
21use std::marker::PhantomData;
22
23use handwired_rs::dsl::{Compiled, HwType, Params};
24use handwired_rs::{Circuit, Range, TensorProgram, Wire};
25use wgpu::util::DeviceExt;
26
27#[derive(Debug)]
28pub enum GpuError {
29 NoAdapter(String),
30 Device(String),
31 Shader(String),
32 Input(String),
33 Map(String),
34}
35
36impl fmt::Display for GpuError {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 GpuError::NoAdapter(message) => write!(formatter, "no GPU adapter: {message}"),
40 GpuError::Device(message) => write!(formatter, "GPU device: {message}"),
41 GpuError::Shader(message) => write!(formatter, "shader: {message}"),
42 GpuError::Input(message) => write!(formatter, "input: {message}"),
43 GpuError::Map(message) => write!(formatter, "reading results: {message}"),
44 }
45 }
46}
47
48impl std::error::Error for GpuError {}
49
50pub struct Gpu {
52 pub device: wgpu::Device,
53 pub queue: wgpu::Queue,
54 pub info: wgpu::AdapterInfo,
55}
56
57impl Gpu {
58 pub fn new() -> Result<Gpu, GpuError> {
62 pollster::block_on(Gpu::create())
63 }
64
65 async fn create() -> Result<Gpu, GpuError> {
66 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::from_env_or_default());
67 let adapter = match std::env::var("WGPU_ADAPTER_NAME") {
68 Ok(name) => {
69 let name = name.to_lowercase();
70 instance
71 .enumerate_adapters(wgpu::Backends::all())
72 .into_iter()
73 .find(|adapter| adapter.get_info().name.to_lowercase().contains(&name))
74 .ok_or_else(|| GpuError::NoAdapter(format!("no adapter named like {name:?}")))?
75 }
76 Err(_) => instance
77 .request_adapter(&wgpu::RequestAdapterOptions {
78 power_preference: wgpu::PowerPreference::HighPerformance,
79 force_fallback_adapter: false,
80 compatible_surface: None,
81 })
82 .await
83 .map_err(|error| GpuError::NoAdapter(error.to_string()))?,
84 };
85 let info = adapter.get_info();
86 if info.device_type == wgpu::DeviceType::Cpu {
87 eprintln!("warning: using the software adapter {:?}; set WGPU_ADAPTER_NAME to choose another", info.name);
88 }
89 let (device, queue) = adapter
90 .request_device(&wgpu::DeviceDescriptor {
91 label: Some("handwired"),
92 required_features: wgpu::Features::empty(),
93 required_limits: adapter.limits(),
94 memory_hints: wgpu::MemoryHints::Performance,
95 trace: wgpu::Trace::Off,
96 })
97 .await
98 .map_err(|error| GpuError::Device(error.to_string()))?;
99 Ok(Gpu { device, queue, info })
100 }
101}
102
103pub struct GpuProgram {
105 pipelines: Vec<wgpu::ComputePipeline>,
106 bind_group_layout: wgpu::BindGroupLayout,
107 tables: wgpu::Buffer,
108 pub source_size: usize,
109 pub output_count: usize,
110 pub operation_count: usize,
112 pub carried: usize,
114 pub stages: Vec<String>,
116 pub dispatch_records: Option<usize>,
119}
120
121impl GpuProgram {
122 pub fn new(gpu: &Gpu, circuit: &Circuit, roots: &[Wire]) -> Result<GpuProgram, GpuError> {
124 let program = circuit.compile(Some(roots));
125 GpuProgram::from_program(gpu, &program)
126 }
127
128 pub fn from_program(gpu: &Gpu, program: &TensorProgram) -> Result<GpuProgram, GpuError> {
129 GpuProgram::from_shader(gpu, wgsl::emit(program))
130 }
131
132 pub fn from_program_staged(gpu: &Gpu, program: &TensorProgram, statements: usize) -> Result<GpuProgram, GpuError> {
135 GpuProgram::from_shader(gpu, wgsl::emit_staged(program, statements))
136 }
137
138 fn from_shader(gpu: &Gpu, shader: wgsl::Shader) -> Result<GpuProgram, GpuError> {
139 let device = &gpu.device;
140 let entry = |binding: u32, ty: wgpu::BufferBindingType| wgpu::BindGroupLayoutEntry {
142 binding,
143 visibility: wgpu::ShaderStages::COMPUTE,
144 ty: wgpu::BindingType::Buffer { ty, has_dynamic_offset: false, min_binding_size: None },
145 count: None,
146 };
147 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
148 label: Some("handwired bindings"),
149 entries: &[
150 entry(0, wgpu::BufferBindingType::Uniform),
151 entry(1, wgpu::BufferBindingType::Storage { read_only: true }),
152 entry(2, wgpu::BufferBindingType::Storage { read_only: true }),
153 entry(3, wgpu::BufferBindingType::Storage { read_only: false }),
154 entry(4, wgpu::BufferBindingType::Storage { read_only: false }),
155 ],
156 });
157 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
158 label: Some("handwired program"),
159 bind_group_layouts: &[&bind_group_layout],
160 push_constant_ranges: &[],
161 });
162 device.push_error_scope(wgpu::ErrorFilter::Validation);
165 let next = std::sync::atomic::AtomicUsize::new(0);
166 let threads = std::thread::available_parallelism().map_or(1, |count| count.get()).min(shader.stages.len());
167 let mut compiled: Vec<(usize, wgpu::ComputePipeline)> = std::thread::scope(|scope| {
168 let workers: Vec<_> = (0..threads)
169 .map(|_| {
170 scope.spawn(|| {
171 let mut pipelines = Vec::new();
172 loop {
173 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
174 let Some(code) = shader.stages.get(index) else { return pipelines };
175 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
176 label: Some("handwired stage"),
177 source: wgpu::ShaderSource::Wgsl(code.as_str().into()),
178 });
179 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
180 label: Some("handwired stage"),
181 layout: Some(&pipeline_layout),
182 module: &module,
183 entry_point: Some("main"),
184 compilation_options: Default::default(),
185 cache: None,
186 });
187 pipelines.push((index, pipeline));
188 }
189 })
190 })
191 .collect();
192 workers.into_iter().flat_map(|worker| worker.join().unwrap()).collect()
193 });
194 compiled.sort_by_key(|&(index, _)| index);
195 let pipelines = compiled.into_iter().map(|(_, pipeline)| pipeline).collect();
196 if let Some(error) = pollster::block_on(device.pop_error_scope()) {
197 return Err(GpuError::Shader(error.to_string()));
198 }
199 let tables = if shader.tables.is_empty() { vec![0.0f32] } else { shader.tables.clone() };
201 let tables = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
202 label: Some("handwired tables"),
203 contents: bytemuck::cast_slice(&tables),
204 usage: wgpu::BufferUsages::STORAGE,
205 });
206 Ok(GpuProgram {
207 pipelines,
208 bind_group_layout,
209 tables,
210 source_size: shader.source_size,
211 output_count: shader.output_count,
212 operation_count: shader.operation_count,
213 carried: shader.carried,
214 stages: shader.stages,
215 dispatch_records: None,
216 })
217 }
218
219 pub fn run(&self, gpu: &Gpu, sources: &[f32]) -> Result<Vec<f32>, GpuError> {
222 if self.source_size == 0 || !sources.len().is_multiple_of(self.source_size) {
223 return Err(GpuError::Input(format!(
224 "{} values isn't a whole number of {}-cell records",
225 sources.len(),
226 self.source_size
227 )));
228 }
229 let limits = gpu.device.limits();
231 let binding = (limits.max_storage_buffer_binding_size as u64).min(limits.max_buffer_size);
232 let cells = self.source_size.max(self.output_count).max(self.carried).max(1) as u64;
233 let fits = (binding / (cells * 4)).max(1) as usize;
234 let per_dispatch = self.dispatch_records.map_or(fits, |records| records.clamp(1, fits));
235 let mut values = Vec::with_capacity(sources.len() / self.source_size * self.output_count);
236 for part in sources.chunks(per_dispatch * self.source_size) {
237 values.extend(self.dispatch(gpu, part)?);
238 }
239 Ok(values)
240 }
241
242 fn dispatch(&self, gpu: &Gpu, sources: &[f32]) -> Result<Vec<f32>, GpuError> {
243 let records = sources.len() / self.source_size;
244 let device = &gpu.device;
245 let workgroups = (records as u32).div_ceil(wgsl::WORKGROUP_SIZE);
246 let limit = device.limits().max_compute_workgroups_per_dimension;
247 let (width, height) =
248 if workgroups <= limit { (workgroups.max(1), 1) } else { (limit, workgroups.div_ceil(limit)) };
249 let params: [u32; 4] = [f32::MIN.to_bits(), records as u32, width * wgsl::WORKGROUP_SIZE, 0];
250 let params = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
251 label: Some("handwired params"),
252 contents: bytemuck::cast_slice(¶ms),
253 usage: wgpu::BufferUsages::UNIFORM,
254 });
255 let source = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
256 label: Some("handwired sources"),
257 contents: bytemuck::cast_slice(sources),
258 usage: wgpu::BufferUsages::STORAGE,
259 });
260 let output_size = (records * self.output_count.max(1) * 4) as u64;
261 let outputs = device.create_buffer(&wgpu::BufferDescriptor {
262 label: Some("handwired outputs"),
263 size: output_size,
264 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
265 mapped_at_creation: false,
266 });
267 let carry = device.create_buffer(&wgpu::BufferDescriptor {
268 label: Some("handwired carried values"),
269 size: (records * self.carried.max(1) * 4) as u64,
270 usage: wgpu::BufferUsages::STORAGE,
271 mapped_at_creation: false,
272 });
273 let readback = device.create_buffer(&wgpu::BufferDescriptor {
274 label: Some("handwired readback"),
275 size: output_size,
276 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
277 mapped_at_creation: false,
278 });
279 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
280 label: Some("handwired bindings"),
281 layout: &self.bind_group_layout,
282 entries: &[
283 wgpu::BindGroupEntry { binding: 0, resource: params.as_entire_binding() },
284 wgpu::BindGroupEntry { binding: 1, resource: source.as_entire_binding() },
285 wgpu::BindGroupEntry { binding: 2, resource: self.tables.as_entire_binding() },
286 wgpu::BindGroupEntry { binding: 3, resource: outputs.as_entire_binding() },
287 wgpu::BindGroupEntry { binding: 4, resource: carry.as_entire_binding() },
288 ],
289 });
290 let mut encoder =
291 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("handwired run") });
292 {
293 let mut pass = encoder
295 .begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("handwired"), timestamp_writes: None });
296 pass.set_bind_group(0, &bind_group, &[]);
297 for pipeline in &self.pipelines {
298 pass.set_pipeline(pipeline);
299 pass.dispatch_workgroups(width, height, 1);
300 }
301 }
302 encoder.copy_buffer_to_buffer(&outputs, 0, &readback, 0, output_size);
303 gpu.queue.submit([encoder.finish()]);
304
305 let slice = readback.slice(..);
306 let (sender, receiver) = std::sync::mpsc::channel();
307 slice.map_async(wgpu::MapMode::Read, move |result| {
308 let _ = sender.send(result);
309 });
310 device.poll(wgpu::PollType::Wait).map_err(|error| GpuError::Map(error.to_string()))?;
311 receiver
312 .recv()
313 .map_err(|error| GpuError::Map(error.to_string()))?
314 .map_err(|error| GpuError::Map(error.to_string()))?;
315 let values = bytemuck::cast_slice::<u8, f32>(&slice.get_mapped_range()).to_vec();
316 readback.unmap();
317 Ok(values)
318 }
319}
320
321pub struct GpuBatch<Args, R> {
323 pub program: GpuProgram,
324 source_ranges: Vec<Range>,
326 marker: PhantomData<fn(Args) -> R>,
327}
328
329impl<Args: Params, R: HwType> GpuBatch<Args, R> {
330 pub fn new(gpu: &Gpu, compiled: &Compiled<Args, R>) -> Result<GpuBatch<Args, R>, GpuError> {
331 Ok(GpuBatch::with_program(compiled, GpuProgram::from_program(gpu, &compiled.program)?))
332 }
333
334 pub fn staged(gpu: &Gpu, compiled: &Compiled<Args, R>, statements: usize) -> Result<GpuBatch<Args, R>, GpuError> {
337 Ok(GpuBatch::with_program(compiled, GpuProgram::from_program_staged(gpu, &compiled.program, statements)?))
338 }
339
340 fn with_program(compiled: &Compiled<Args, R>, program: GpuProgram) -> GpuBatch<Args, R> {
341 let circuit = &compiled.circuit;
342 let source_ranges = (0..circuit.source_size()).map(|cell| circuit.range(circuit.source(cell))).collect();
343 GpuBatch { program, source_ranges, marker: PhantomData }
344 }
345
346 pub fn encode(&self, records: &[Args]) -> Result<Vec<f32>, GpuError> {
348 let mut sources = Vec::with_capacity(records.len() * Args::CELLS);
349 for (record_index, record) in records.iter().enumerate() {
350 record.encode(&mut sources);
351 let start = record_index * Args::CELLS;
352 for (cell, (&value, range)) in sources[start..].iter().zip(&self.source_ranges).enumerate() {
353 if !(value as f64 >= range.min && value as f64 <= range.max) {
354 return Err(GpuError::Input(format!(
355 "record {record_index}: source cell {cell} is {value}, outside its declared range {}..{}",
356 range.min, range.max
357 )));
358 }
359 }
360 }
361 Ok(sources)
362 }
363
364 pub fn run(&self, gpu: &Gpu, records: &[Args]) -> Result<Vec<R>, GpuError> {
365 let outputs = self.program.run(gpu, &self.encode(records)?)?;
366 Ok(outputs
367 .chunks(self.program.output_count.max(1))
368 .take(records.len())
369 .map(|record| R::decode(&mut record.iter().copied()))
370 .collect())
371 }
372}