1pub mod wgsl;
18
19use std::fmt;
20use std::marker::PhantomData;
21
22use handwired_rs::dsl::{Compiled, HwType, Params};
23use handwired_rs::{Circuit, TensorProgram, Wire};
24use wgpu::util::DeviceExt;
25
26#[derive(Debug)]
27pub enum GpuError {
28 NoAdapter(String),
29 Device(String),
30 Shader(String),
31 Input(String),
32 Map(String),
33}
34
35impl fmt::Display for GpuError {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 GpuError::NoAdapter(message) => write!(formatter, "no GPU adapter: {message}"),
39 GpuError::Device(message) => write!(formatter, "GPU device: {message}"),
40 GpuError::Shader(message) => write!(formatter, "shader: {message}"),
41 GpuError::Input(message) => write!(formatter, "input: {message}"),
42 GpuError::Map(message) => write!(formatter, "reading results: {message}"),
43 }
44 }
45}
46
47impl std::error::Error for GpuError {}
48
49pub struct Gpu {
51 pub device: wgpu::Device,
52 pub queue: wgpu::Queue,
53 pub info: wgpu::AdapterInfo,
54}
55
56impl Gpu {
57 pub fn new() -> Result<Gpu, GpuError> {
61 pollster::block_on(Gpu::create())
62 }
63
64 async fn create() -> Result<Gpu, GpuError> {
65 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::from_env_or_default());
66 let adapter = match std::env::var("WGPU_ADAPTER_NAME") {
67 Ok(name) => {
68 let name = name.to_lowercase();
69 instance
70 .enumerate_adapters(wgpu::Backends::all())
71 .into_iter()
72 .find(|adapter| adapter.get_info().name.to_lowercase().contains(&name))
73 .ok_or_else(|| GpuError::NoAdapter(format!("no adapter named like {name:?}")))?
74 }
75 Err(_) => instance
76 .request_adapter(&wgpu::RequestAdapterOptions {
77 power_preference: wgpu::PowerPreference::HighPerformance,
78 force_fallback_adapter: false,
79 compatible_surface: None,
80 })
81 .await
82 .map_err(|error| GpuError::NoAdapter(error.to_string()))?,
83 };
84 let info = adapter.get_info();
85 if info.device_type == wgpu::DeviceType::Cpu {
86 eprintln!("warning: using the software adapter {:?}; set WGPU_ADAPTER_NAME to choose another", info.name);
87 }
88 let (device, queue) = adapter
89 .request_device(&wgpu::DeviceDescriptor {
90 label: Some("handwired"),
91 required_features: wgpu::Features::empty(),
92 required_limits: adapter.limits(),
93 memory_hints: wgpu::MemoryHints::Performance,
94 trace: wgpu::Trace::Off,
95 })
96 .await
97 .map_err(|error| GpuError::Device(error.to_string()))?;
98 Ok(Gpu { device, queue, info })
99 }
100}
101
102pub struct GpuProgram {
104 pipeline: wgpu::ComputePipeline,
105 bind_group_layout: wgpu::BindGroupLayout,
106 tables: wgpu::Buffer,
107 pub source_size: usize,
108 pub output_count: usize,
109 pub operation_count: usize,
111 pub wgsl: String,
112}
113
114impl GpuProgram {
115 pub fn new(gpu: &Gpu, circuit: &Circuit, roots: &[Wire]) -> Result<GpuProgram, GpuError> {
117 let program = circuit.compile(Some(roots));
118 GpuProgram::from_program(gpu, &program)
119 }
120
121 pub fn from_program(gpu: &Gpu, program: &TensorProgram) -> Result<GpuProgram, GpuError> {
122 let shader = wgsl::emit(program);
123 let device = &gpu.device;
124 device.push_error_scope(wgpu::ErrorFilter::Validation);
125 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
126 label: Some("handwired program"),
127 source: wgpu::ShaderSource::Wgsl(shader.code.as_str().into()),
128 });
129 let entry = |binding: u32, ty: wgpu::BufferBindingType| wgpu::BindGroupLayoutEntry {
131 binding,
132 visibility: wgpu::ShaderStages::COMPUTE,
133 ty: wgpu::BindingType::Buffer { ty, has_dynamic_offset: false, min_binding_size: None },
134 count: None,
135 };
136 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
137 label: Some("handwired bindings"),
138 entries: &[
139 entry(0, wgpu::BufferBindingType::Uniform),
140 entry(1, wgpu::BufferBindingType::Storage { read_only: true }),
141 entry(2, wgpu::BufferBindingType::Storage { read_only: true }),
142 entry(3, wgpu::BufferBindingType::Storage { read_only: false }),
143 ],
144 });
145 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
146 label: Some("handwired program"),
147 bind_group_layouts: &[&bind_group_layout],
148 push_constant_ranges: &[],
149 });
150 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
151 label: Some("handwired program"),
152 layout: Some(&pipeline_layout),
153 module: &module,
154 entry_point: Some("main"),
155 compilation_options: Default::default(),
156 cache: None,
157 });
158 if let Some(error) = pollster::block_on(device.pop_error_scope()) {
159 return Err(GpuError::Shader(error.to_string()));
160 }
161 let tables = if shader.tables.is_empty() { vec![0.0f32] } else { shader.tables.clone() };
163 let tables = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
164 label: Some("handwired tables"),
165 contents: bytemuck::cast_slice(&tables),
166 usage: wgpu::BufferUsages::STORAGE,
167 });
168 Ok(GpuProgram {
169 pipeline,
170 bind_group_layout,
171 tables,
172 source_size: shader.source_size,
173 output_count: shader.output_count,
174 operation_count: shader.operation_count,
175 wgsl: shader.code,
176 })
177 }
178
179 pub fn run(&self, gpu: &Gpu, sources: &[f32]) -> Result<Vec<f32>, GpuError> {
182 if self.source_size == 0 || !sources.len().is_multiple_of(self.source_size) {
183 return Err(GpuError::Input(format!(
184 "{} values isn't a whole number of {}-cell records",
185 sources.len(),
186 self.source_size
187 )));
188 }
189 let records = sources.len() / self.source_size;
190 let device = &gpu.device;
191 let workgroups = (records as u32).div_ceil(wgsl::WORKGROUP_SIZE);
192 let limit = device.limits().max_compute_workgroups_per_dimension;
193 let (width, height) =
194 if workgroups <= limit { (workgroups.max(1), 1) } else { (limit, workgroups.div_ceil(limit)) };
195 let params: [u32; 4] = [f32::MIN.to_bits(), records as u32, width * wgsl::WORKGROUP_SIZE, 0];
196 let params = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
197 label: Some("handwired params"),
198 contents: bytemuck::cast_slice(¶ms),
199 usage: wgpu::BufferUsages::UNIFORM,
200 });
201 let source = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
202 label: Some("handwired sources"),
203 contents: bytemuck::cast_slice(sources),
204 usage: wgpu::BufferUsages::STORAGE,
205 });
206 let output_size = (records * self.output_count.max(1) * 4) as u64;
207 let outputs = device.create_buffer(&wgpu::BufferDescriptor {
208 label: Some("handwired outputs"),
209 size: output_size,
210 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
211 mapped_at_creation: false,
212 });
213 let readback = device.create_buffer(&wgpu::BufferDescriptor {
214 label: Some("handwired readback"),
215 size: output_size,
216 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
217 mapped_at_creation: false,
218 });
219 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
220 label: Some("handwired bindings"),
221 layout: &self.bind_group_layout,
222 entries: &[
223 wgpu::BindGroupEntry { binding: 0, resource: params.as_entire_binding() },
224 wgpu::BindGroupEntry { binding: 1, resource: source.as_entire_binding() },
225 wgpu::BindGroupEntry { binding: 2, resource: self.tables.as_entire_binding() },
226 wgpu::BindGroupEntry { binding: 3, resource: outputs.as_entire_binding() },
227 ],
228 });
229 let mut encoder =
230 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("handwired run") });
231 {
232 let mut pass = encoder
233 .begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("handwired"), timestamp_writes: None });
234 pass.set_pipeline(&self.pipeline);
235 pass.set_bind_group(0, &bind_group, &[]);
236 pass.dispatch_workgroups(width, height, 1);
237 }
238 encoder.copy_buffer_to_buffer(&outputs, 0, &readback, 0, output_size);
239 gpu.queue.submit([encoder.finish()]);
240
241 let slice = readback.slice(..);
242 let (sender, receiver) = std::sync::mpsc::channel();
243 slice.map_async(wgpu::MapMode::Read, move |result| {
244 let _ = sender.send(result);
245 });
246 device.poll(wgpu::PollType::Wait).map_err(|error| GpuError::Map(error.to_string()))?;
247 receiver
248 .recv()
249 .map_err(|error| GpuError::Map(error.to_string()))?
250 .map_err(|error| GpuError::Map(error.to_string()))?;
251 let values = bytemuck::cast_slice::<u8, f32>(&slice.get_mapped_range()).to_vec();
252 readback.unmap();
253 Ok(values)
254 }
255}
256
257pub struct GpuBatch<Args, R> {
259 pub program: GpuProgram,
260 marker: PhantomData<fn(Args) -> R>,
261}
262
263impl<Args: Params, R: HwType> GpuBatch<Args, R> {
264 pub fn new(gpu: &Gpu, compiled: &Compiled<Args, R>) -> Result<GpuBatch<Args, R>, GpuError> {
265 let program = GpuProgram::from_program(gpu, &compiled.program)?;
266 Ok(GpuBatch { program, marker: PhantomData })
267 }
268
269 pub fn encode(&self, records: &[Args]) -> Vec<f32> {
271 let mut sources = Vec::with_capacity(records.len() * Args::CELLS);
272 for record in records {
273 record.encode(&mut sources);
274 }
275 sources
276 }
277
278 pub fn run(&self, gpu: &Gpu, records: &[Args]) -> Result<Vec<R>, GpuError> {
279 let outputs = self.program.run(gpu, &self.encode(records))?;
280 Ok(outputs
281 .chunks(self.program.output_count.max(1))
282 .take(records.len())
283 .map(|record| R::decode(&mut record.iter().copied()))
284 .collect())
285 }
286}