1use std::collections::HashMap;
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex};
8
9use wgpu::util::DeviceExt;
10
11use crate::error::{ForgeError, Result};
12
13pub mod ops;
14
15pub const OFFSET_ALIGN_BYTES: usize = 256;
17
18const SHADERS_CORE: &[(&str, &str)] = &[
20 ("add", include_str!("../../../shaders/add.wgsl")),
21 ("gelu", include_str!("../../../shaders/gelu.wgsl")),
22 ("matmul", include_str!("../../../shaders/matmul.wgsl")),
23 ("softmax", include_str!("../../../shaders/softmax.wgsl")),
24 ("layernorm", include_str!("../../../shaders/layernorm.wgsl")),
25 ("embedding", include_str!("../../../shaders/embedding.wgsl")),
26 (
27 "split_heads",
28 include_str!("../../../shaders/split_heads.wgsl"),
29 ),
30 (
31 "merge_heads",
32 include_str!("../../../shaders/merge_heads.wgsl"),
33 ),
34 ("kv_append", include_str!("../../../shaders/kv_append.wgsl")),
35];
36
37#[cfg(feature = "train")]
44const SHADERS_TRAIN: &[(&str, &str)] = &[
45 ("gelu_bwd", include_str!("../../../shaders/gelu_bwd.wgsl")),
46 (
47 "softmax_bwd",
48 include_str!("../../../shaders/softmax_bwd.wgsl"),
49 ),
50 (
51 "layernorm_bwd_dx",
52 include_str!("../../../shaders/layernorm_bwd_dx.wgsl"),
53 ),
54 (
55 "layernorm_bwd_dp",
56 include_str!("../../../shaders/layernorm_bwd_dp.wgsl"),
57 ),
58 ("sum_rows", include_str!("../../../shaders/sum_rows.wgsl")),
59 (
60 "scatter_add",
61 include_str!("../../../shaders/scatter_add.wgsl"),
62 ),
63 (
64 "gather_nll",
65 include_str!("../../../shaders/gather_nll.wgsl"),
66 ),
67 ("ce_bwd", include_str!("../../../shaders/ce_bwd.wgsl")),
68 ("dropout", include_str!("../../../shaders/dropout.wgsl")),
69 (
70 "unsplit_heads",
71 include_str!("../../../shaders/unsplit_heads.wgsl"),
72 ),
73 (
74 "unmerge_heads",
75 include_str!("../../../shaders/unmerge_heads.wgsl"),
76 ),
77 ("sumsq", include_str!("../../../shaders/sumsq.wgsl")),
78 ("scale", include_str!("../../../shaders/scale.wgsl")),
79 ("adamw", include_str!("../../../shaders/adamw.wgsl")),
80];
81
82#[cfg(not(feature = "train"))]
83const SHADERS_TRAIN: &[(&str, &str)] = &[];
84
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96pub struct Stats {
97 pub dispatches: usize,
98 pub submits: usize,
99 pub buffers_created: usize,
100 pub bytes_allocated: usize,
101}
102
103impl Stats {
104 pub fn since(self, earlier: Stats) -> Stats {
106 Stats {
107 dispatches: self.dispatches - earlier.dispatches,
108 submits: self.submits - earlier.submits,
109 buffers_created: self.buffers_created - earlier.buffers_created,
110 bytes_allocated: self.bytes_allocated - earlier.bytes_allocated,
111 }
112 }
113}
114
115#[derive(Default)]
116struct Counters {
117 dispatches: AtomicUsize,
118 submits: AtomicUsize,
119 buffers_created: AtomicUsize,
120 bytes_allocated: AtomicUsize,
121}
122
123#[derive(Default)]
130struct Pool {
131 free: HashMap<u64, Vec<wgpu::Buffer>>,
132 bytes: usize,
133}
134
135const MAX_POOL_BYTES: usize = 512 * 1024 * 1024;
139
140pub struct PooledBuffer {
163 buf: Option<wgpu::Buffer>,
164 ctx: Arc<WgpuContext>,
165 size: u64,
166 recycle: bool,
167}
168
169impl std::ops::Deref for PooledBuffer {
170 type Target = wgpu::Buffer;
171 fn deref(&self) -> &wgpu::Buffer {
172 self.buf.as_ref().expect("PooledBuffer used after drop")
174 }
175}
176
177impl std::fmt::Debug for PooledBuffer {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 write!(f, "PooledBuffer({} B)", self.size)
180 }
181}
182
183impl Drop for PooledBuffer {
184 fn drop(&mut self) {
185 let Some(buf) = self.buf.take() else { return };
186 if !self.recycle {
187 return;
188 }
189 let mut pool = self.ctx.pool.lock().unwrap();
190 if pool.bytes + self.size as usize > MAX_POOL_BYTES {
191 return; }
193 pool.bytes += self.size as usize;
194 pool.free.entry(self.size).or_default().push(buf);
195 }
196}
197
198pub struct DispatchScope {
205 ctx: Arc<WgpuContext>,
206}
207
208impl Drop for DispatchScope {
209 fn drop(&mut self) {
210 let outermost = {
211 let mut s = self.ctx.scope.lock().unwrap();
212 s.depth -= 1;
213 s.depth == 0
214 };
215 if outermost {
216 self.ctx.flush();
217 }
218 }
219}
220
221#[derive(Default)]
222struct ScopeState {
223 encoder: Option<wgpu::CommandEncoder>,
224 pass: Option<wgpu::ComputePass<'static>>,
237 depth: usize,
238}
239
240type Kernel = (Arc<wgpu::ComputePipeline>, Arc<wgpu::BindGroupLayout>);
247
248pub struct WgpuContext {
250 pub device: wgpu::Device,
251 pub queue: wgpu::Queue,
252 pub adapter_info: wgpu::AdapterInfo,
253 pipelines: Mutex<HashMap<&'static str, Kernel>>,
254 counters: Counters,
255 pool: Mutex<Pool>,
256 scope: Mutex<ScopeState>,
257}
258
259impl std::fmt::Debug for WgpuContext {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 write!(f, "WgpuContext({})", self.adapter_info.name)
262 }
263}
264
265impl WgpuContext {
266 #[cfg(not(target_arch = "wasm32"))]
268 pub fn new() -> Result<Arc<Self>> {
269 pollster::block_on(Self::new_async())
270 }
271
272 pub async fn new_async() -> Result<Arc<Self>> {
275 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
276 let adapter = instance
277 .request_adapter(&wgpu::RequestAdapterOptions {
278 power_preference: wgpu::PowerPreference::HighPerformance,
279 ..Default::default()
280 })
281 .await
282 .map_err(|e| ForgeError::Wgpu(format!("no adapter: {e}")))?;
283 let adapter_info = adapter.get_info();
284 let (device, queue) = adapter
287 .request_device(&wgpu::DeviceDescriptor {
288 label: Some("forge"),
289 required_limits: adapter.limits(),
290 ..Default::default()
291 })
292 .await
293 .map_err(|e| ForgeError::Wgpu(format!("request_device: {e}")))?;
294 Ok(Arc::new(WgpuContext {
295 device,
296 queue,
297 adapter_info,
298 pipelines: Mutex::new(HashMap::new()),
299 counters: Counters::default(),
300 pool: Mutex::new(Pool::default()),
301 scope: Mutex::new(ScopeState::default()),
302 }))
303 }
304
305 pub fn scope(self: &Arc<Self>) -> DispatchScope {
317 let mut s = self.scope.lock().unwrap();
318 s.depth += 1;
319 DispatchScope { ctx: self.clone() }
320 }
321
322 pub fn flush(&self) {
330 let encoder = {
331 let mut s = self.scope.lock().unwrap();
332 s.pass = None; s.encoder.take()
334 };
335 if let Some(encoder) = encoder {
336 self.submit(encoder);
337 }
338 }
339
340 fn submit_with_copies(&self, copies: &[(&wgpu::Buffer, u64, &wgpu::Buffer, u64)]) {
351 let mut encoder = {
352 let mut s = self.scope.lock().unwrap();
353 s.pass = None;
354 s.encoder.take()
355 }
356 .unwrap_or_else(|| self.device.create_command_encoder(&Default::default()));
357 for (src, src_off, dst, size) in copies {
358 encoder.copy_buffer_to_buffer(src, *src_off, dst, 0, *size);
359 }
360 self.submit(encoder);
361 }
362
363 pub fn create_pooled(self: &Arc<Self>, size_bytes: usize) -> PooledBuffer {
366 let size = size_bytes.max(4).next_multiple_of(4) as u64;
367 let recycled = {
368 let mut pool = self.pool.lock().unwrap();
369 let hit = pool.free.get_mut(&size).and_then(Vec::pop);
370 if hit.is_some() {
371 pool.bytes -= size as usize;
372 }
373 hit
374 };
375 let buf = recycled.unwrap_or_else(|| self.create_storage(size as usize));
376 PooledBuffer {
377 buf: Some(buf),
378 ctx: self.clone(),
379 size,
380 recycle: true,
381 }
382 }
383
384 pub fn stats(&self) -> Stats {
387 Stats {
388 dispatches: self.counters.dispatches.load(Ordering::Relaxed),
389 submits: self.counters.submits.load(Ordering::Relaxed),
390 buffers_created: self.counters.buffers_created.load(Ordering::Relaxed),
391 bytes_allocated: self.counters.bytes_allocated.load(Ordering::Relaxed),
392 }
393 }
394
395 fn submit(&self, encoder: wgpu::CommandEncoder) {
398 self.counters.submits.fetch_add(1, Ordering::Relaxed);
399 self.queue.submit([encoder.finish()]);
400 }
401
402 fn pipeline(&self, name: &'static str) -> Kernel {
410 let mut cache = self.pipelines.lock().unwrap();
411 cache
412 .entry(name)
413 .or_insert_with(|| {
414 let src = SHADERS_CORE
415 .iter()
416 .chain(SHADERS_TRAIN)
417 .find(|(n, _)| *n == name)
418 .unwrap_or_else(|| panic!("unknown shader {name}"))
419 .1;
420 let module = self
421 .device
422 .create_shader_module(wgpu::ShaderModuleDescriptor {
423 label: Some(name),
424 source: wgpu::ShaderSource::Wgsl(src.into()),
425 });
426 let pipeline =
427 self.device
428 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
429 label: Some(name),
430 layout: None,
431 module: &module,
432 entry_point: Some("main"),
433 compilation_options: Default::default(),
434 cache: None,
435 });
436 let layout = Arc::new(pipeline.get_bind_group_layout(0));
437 (Arc::new(pipeline), layout)
438 })
439 .clone()
440 }
441
442 pub fn create_storage(&self, size_bytes: usize) -> wgpu::Buffer {
443 let size = size_bytes.max(4) as u64;
444 self.counters
445 .buffers_created
446 .fetch_add(1, Ordering::Relaxed);
447 self.counters
448 .bytes_allocated
449 .fetch_add(size as usize, Ordering::Relaxed);
450 self.device.create_buffer(&wgpu::BufferDescriptor {
451 label: None,
452 size,
453 usage: wgpu::BufferUsages::STORAGE
454 | wgpu::BufferUsages::COPY_DST
455 | wgpu::BufferUsages::COPY_SRC,
456 mapped_at_creation: false,
457 })
458 }
459
460 pub fn create_zeroed(self: &Arc<Self>, size_bytes: usize) -> PooledBuffer {
472 PooledBuffer {
473 buf: Some(self.create_storage(size_bytes.max(4))),
474 ctx: self.clone(),
475 size: size_bytes as u64,
476 recycle: false,
477 }
478 }
479
480 pub fn upload(self: &Arc<Self>, bytes: &[u8]) -> PooledBuffer {
487 let buf = self.create_storage(bytes.len().max(4));
488 self.queue.write_buffer(&buf, 0, bytes);
489 PooledBuffer {
490 buf: Some(buf),
491 ctx: self.clone(),
492 size: bytes.len() as u64,
493 recycle: false,
494 }
495 }
496
497 fn stage_copy(
498 &self,
499 buf: &wgpu::Buffer,
500 offset_bytes: usize,
501 size_bytes: usize,
502 ) -> wgpu::Buffer {
503 let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
504 label: None,
505 size: size_bytes as u64,
506 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
507 mapped_at_creation: false,
508 });
509 self.submit_with_copies(&[(buf, offset_bytes as u64, &staging, size_bytes as u64)]);
514 staging
515 }
516
517 #[cfg(not(target_arch = "wasm32"))]
520 pub fn readback(
521 &self,
522 buf: &wgpu::Buffer,
523 offset_bytes: usize,
524 size_bytes: usize,
525 ) -> Result<Vec<u8>> {
526 let staging = self.stage_copy(buf, offset_bytes, size_bytes);
527 let slice = staging.slice(..);
528 let (tx, rx) = std::sync::mpsc::channel();
529 slice.map_async(wgpu::MapMode::Read, move |r| {
530 let _ = tx.send(r);
531 });
532 self.device
533 .poll(wgpu::PollType::Wait)
534 .map_err(|e| ForgeError::Wgpu(format!("poll: {e:?}")))?;
535 rx.recv()
536 .map_err(|_| ForgeError::Wgpu("map_async callback dropped".into()))?
537 .map_err(|e| ForgeError::Wgpu(format!("map_async: {e:?}")))?;
538 let out = slice.get_mapped_range().to_vec();
539 staging.unmap();
540 Ok(out)
541 }
542
543 pub async fn readback_async(
546 &self,
547 buf: &wgpu::Buffer,
548 offset_bytes: usize,
549 size_bytes: usize,
550 ) -> Result<Vec<u8>> {
551 let staging = self.stage_copy(buf, offset_bytes, size_bytes);
552 let slice = staging.slice(..);
553 let (tx, rx) = oneshot::channel();
554 slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r));
555 #[cfg(not(target_arch = "wasm32"))]
556 self.device
557 .poll(wgpu::PollType::Wait)
558 .map_err(|e| ForgeError::Wgpu(format!("poll: {e:?}")))?;
559 #[cfg(target_arch = "wasm32")]
560 let _ = self.device.poll(wgpu::PollType::Poll);
561 rx.await
562 .map_err(|e| ForgeError::Wgpu(format!("map_async: {e:?}")))?;
563 let out = slice.get_mapped_range().to_vec();
564 staging.unmap();
565 Ok(out)
566 }
567
568 pub async fn readback_many_async(
578 &self,
579 regions: &[(&wgpu::Buffer, usize, usize)],
580 ) -> Result<Vec<Vec<u8>>> {
581 if regions.is_empty() {
582 return Ok(Vec::new());
583 }
584 let staging: Vec<wgpu::Buffer> = regions
585 .iter()
586 .map(|(_, _, size_bytes)| {
587 self.device.create_buffer(&wgpu::BufferDescriptor {
588 label: None,
589 size: *size_bytes as u64,
590 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
591 mapped_at_creation: false,
592 })
593 })
594 .collect();
595 let copies: Vec<_> = regions
598 .iter()
599 .zip(&staging)
600 .map(|((buf, off, size), s)| (*buf, *off as u64, s, *size as u64))
601 .collect();
602 self.submit_with_copies(&copies);
603
604 let waits: Vec<_> = staging
607 .iter()
608 .map(|s| {
609 let (tx, rx) = oneshot::channel();
610 s.slice(..)
611 .map_async(wgpu::MapMode::Read, move |r| tx.send(r));
612 rx
613 })
614 .collect();
615 #[cfg(not(target_arch = "wasm32"))]
616 self.device
617 .poll(wgpu::PollType::Wait)
618 .map_err(|e| ForgeError::Wgpu(format!("poll: {e:?}")))?;
619 #[cfg(target_arch = "wasm32")]
620 let _ = self.device.poll(wgpu::PollType::Poll);
621
622 let mut out = Vec::with_capacity(regions.len());
623 for (rx, s) in waits.into_iter().zip(&staging) {
624 rx.await
625 .map_err(|e| ForgeError::Wgpu(format!("map_async: {e:?}")))?;
626 out.push(s.slice(..).get_mapped_range().to_vec());
627 s.unmap();
628 }
629 Ok(out)
630 }
631
632 pub fn dispatch(
636 &self,
637 name: &'static str,
638 params: &[u32],
639 buffers: &[(&wgpu::Buffer, usize, usize)],
640 workgroups: (u32, u32, u32),
641 ) {
642 self.counters.dispatches.fetch_add(1, Ordering::Relaxed);
643 let (pipeline, layout) = self.pipeline(name);
644 let params_buf = self
645 .device
646 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
647 label: Some(name),
648 contents: bytemuck::cast_slice(params),
649 usage: wgpu::BufferUsages::UNIFORM,
650 });
651 let mut entries = vec![wgpu::BindGroupEntry {
652 binding: 0,
653 resource: params_buf.as_entire_binding(),
654 }];
655 for (i, (buf, off, size)) in buffers.iter().enumerate() {
656 debug_assert!(off % OFFSET_ALIGN_BYTES == 0, "storage offset misaligned");
657 entries.push(wgpu::BindGroupEntry {
658 binding: (i + 1) as u32,
659 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
660 buffer: buf,
661 offset: *off as u64,
662 size: Some(std::num::NonZeroU64::new((*size).max(4) as u64).unwrap()),
663 }),
664 });
665 }
666 let bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
667 label: Some(name),
668 layout: &layout,
669 entries: &entries,
670 });
671 let mut scope = self.scope.lock().unwrap();
676 if scope.depth > 0 {
677 let ScopeState { encoder, pass, .. } = &mut *scope;
678 let encoder = encoder
680 .get_or_insert_with(|| self.device.create_command_encoder(&Default::default()));
681 let pass = match pass {
682 Some(p) => p,
683 None => pass.insert(
684 encoder
685 .begin_compute_pass(&Default::default())
686 .forget_lifetime(),
687 ),
688 };
689 pass.set_pipeline(&pipeline);
690 pass.set_bind_group(0, &bind, &[]);
691 pass.dispatch_workgroups(workgroups.0, workgroups.1, workgroups.2);
692 return;
693 }
694 drop(scope);
695
696 let mut encoder = self.device.create_command_encoder(&Default::default());
697 {
698 let mut pass = encoder.begin_compute_pass(&Default::default());
699 pass.set_pipeline(&pipeline);
700 pass.set_bind_group(0, &bind, &[]);
701 pass.dispatch_workgroups(workgroups.0, workgroups.1, workgroups.2);
702 }
703 self.submit(encoder);
704 }
705}
706
707mod oneshot {
711 use std::future::Future;
712 use std::pin::Pin;
713 use std::sync::{Arc, Mutex};
714 use std::task::{Context, Poll, Waker};
715
716 struct State<T> {
717 value: Option<T>,
718 waker: Option<Waker>,
719 }
720
721 pub struct Sender<T>(Arc<Mutex<State<T>>>);
722 pub struct Receiver<T>(Arc<Mutex<State<T>>>);
723
724 pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
725 let shared = Arc::new(Mutex::new(State {
726 value: None,
727 waker: None,
728 }));
729 (Sender(shared.clone()), Receiver(shared))
730 }
731
732 impl<T> Sender<T> {
733 pub fn send(self, value: T) {
734 let mut s = self.0.lock().unwrap();
735 s.value = Some(value);
736 if let Some(w) = s.waker.take() {
737 w.wake();
738 }
739 }
740 }
741
742 impl<T> Future for Receiver<T> {
743 type Output = T;
744 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
745 let mut s = self.0.lock().unwrap();
746 match s.value.take() {
747 Some(v) => Poll::Ready(v),
748 None => {
749 s.waker = Some(cx.waker().clone());
750 Poll::Pending
751 }
752 }
753 }
754 }
755}
756
757pub fn linear_grid(numel: usize) -> (u32, u32, u32) {
760 let groups = numel.div_ceil(256).max(1) as u32;
761 if groups <= 65535 {
762 (groups, 1, 1)
763 } else {
764 let y = groups.div_ceil(65535);
765 (65535, y, 1)
766 }
767}