1use crate::ternary::{rebake_ternary_blob_to_2bit, TERNARY_GEMM_2BIT_WGSL, TERNARY_GEMM_WGSL};
12use std::collections::HashMap;
13
14fn ternary_params_bytes(n_in: u32, n_out: u32, n_batch: u32, scale: f32) -> [u8; 32] {
17 let mut b = [0u8; 32];
18 b[0..4].copy_from_slice(&n_in.to_le_bytes());
19 b[4..8].copy_from_slice(&n_out.to_le_bytes());
20 b[8..12].copy_from_slice(&n_batch.to_le_bytes());
21 b[20..24].copy_from_slice(&scale.to_le_bytes());
23 b
24}
25
26pub fn ternary_gemm_gpu(
28 device: &wgpu::Device,
29 queue: &wgpu::Queue,
30 activations: &[f32],
31 packed: &[u8],
32 scale: f32,
33 n_in: usize,
34 n_out: usize,
35 n_batch: usize,
36) -> Vec<f32> {
37 run_gemm(
38 device,
39 queue,
40 TERNARY_GEMM_WGSL,
41 activations,
42 packed,
43 scale,
44 n_in,
45 n_out,
46 n_batch,
47 )
48}
49
50pub fn ternary_gemm_gpu_2bit(
53 device: &wgpu::Device,
54 queue: &wgpu::Queue,
55 activations: &[f32],
56 packed: &[u8],
57 scale: f32,
58 n_in: usize,
59 n_out: usize,
60 n_batch: usize,
61) -> Vec<f32> {
62 run_gemm(
63 device,
64 queue,
65 TERNARY_GEMM_2BIT_WGSL,
66 activations,
67 packed,
68 scale,
69 n_in,
70 n_out,
71 n_batch,
72 )
73}
74
75#[allow(clippy::too_many_arguments)]
78fn run_gemm(
79 device: &wgpu::Device,
80 queue: &wgpu::Queue,
81 wgsl: &str,
82 activations: &[f32],
83 packed: &[u8],
84 scale: f32,
85 n_in: usize,
86 n_out: usize,
87 n_batch: usize,
88) -> Vec<f32> {
89 let n_batch = n_batch.max(1);
90 let out_elems = n_batch * n_out;
91
92 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
93 label: Some("ternary_gemm"),
94 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
95 });
96 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
97 label: Some("ternary_gemm_pipeline"),
98 layout: None, module: &shader,
100 entry_point: Some("ternary_gemm"),
101 compilation_options: Default::default(),
102 cache: None,
103 });
104
105 let mk_buf = |label: &str, contents: &[u8], usage: wgpu::BufferUsages| {
106 let buf = device.create_buffer(&wgpu::BufferDescriptor {
107 label: Some(label),
108 size: contents.len().max(4) as u64,
109 usage,
110 mapped_at_creation: false,
111 });
112 if !contents.is_empty() {
113 queue.write_buffer(&buf, 0, contents);
114 }
115 buf
116 };
117
118 let act_buf = mk_buf(
119 "ternary_act",
120 bytemuck::cast_slice(activations),
121 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
122 );
123 let mut trits = packed.to_vec();
125 while trits.len() % 4 != 0 || trits.is_empty() {
126 trits.push(0);
127 }
128 let trit_buf = mk_buf(
129 "ternary_trits",
130 &trits,
131 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
132 );
133 let params = ternary_params_bytes(n_in as u32, n_out as u32, n_batch as u32, scale);
134 let param_buf = mk_buf(
135 "ternary_params",
136 ¶ms,
137 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
138 );
139 let out_buf = device.create_buffer(&wgpu::BufferDescriptor {
140 label: Some("ternary_out"),
141 size: (out_elems * 4).max(4) as u64,
142 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
143 mapped_at_creation: false,
144 });
145 let staging = device.create_buffer(&wgpu::BufferDescriptor {
146 label: Some("ternary_staging"),
147 size: (out_elems * 4).max(4) as u64,
148 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
149 mapped_at_creation: false,
150 });
151
152 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
153 label: Some("ternary_bind"),
154 layout: &pipeline.get_bind_group_layout(0),
155 entries: &[
156 wgpu::BindGroupEntry {
157 binding: 0,
158 resource: act_buf.as_entire_binding(),
159 },
160 wgpu::BindGroupEntry {
161 binding: 1,
162 resource: trit_buf.as_entire_binding(),
163 },
164 wgpu::BindGroupEntry {
165 binding: 2,
166 resource: param_buf.as_entire_binding(),
167 },
168 wgpu::BindGroupEntry {
169 binding: 3,
170 resource: out_buf.as_entire_binding(),
171 },
172 ],
173 });
174
175 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
176 label: Some("ternary_enc"),
177 });
178 {
179 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
180 label: Some("ternary_pass"),
181 timestamp_writes: None,
182 });
183 pass.set_pipeline(&pipeline);
184 pass.set_bind_group(0, &bind, &[]);
185 let wg_x = (n_out as u32).div_ceil(64).max(1);
186 pass.dispatch_workgroups(wg_x, n_batch as u32, 1);
187 }
188 encoder.copy_buffer_to_buffer(&out_buf, 0, &staging, 0, (out_elems * 4).max(4) as u64);
189 queue.submit(Some(encoder.finish()));
190
191 let slice = staging.slice(..);
192 let (tx, rx) = std::sync::mpsc::channel();
193 slice.map_async(wgpu::MapMode::Read, move |r| {
194 let _ = tx.send(r);
195 });
196 let _ = device.poll(wgpu::PollType::wait_indefinitely());
197 rx.recv()
198 .expect("map channel")
199 .expect("map ternary staging");
200 let data = slice
201 .get_mapped_range()
202 .expect("wgpu buffer map_range failed");
203 let out: Vec<f32> = bytemuck::cast_slice(&data)[..out_elems].to_vec();
204 drop(data);
205 staging.unmap();
206 out
207}
208
209const TERNARY_RESIDENT_ALIGN: u64 = 256;
215
216#[inline]
217fn align_up_u64(x: u64, align: u64) -> u64 {
218 (x + align - 1) / align * align
219}
220
221#[derive(Clone, Copy, Debug)]
223struct ResidentTernaryTensor {
224 gpu_offset: u64,
226 packed_len: u32,
228 n_in: u32,
229 n_out: u32,
230 scale: f32,
232}
233
234pub struct TernaryFfnResident {
247 pipeline: wgpu::ComputePipeline,
248 weights: wgpu::Buffer,
249 act_buf: wgpu::Buffer,
250 out_buf: wgpu::Buffer,
251 params_buf: wgpu::Buffer,
252 staging: wgpu::Buffer,
253 map: HashMap<u64, ResidentTernaryTensor>,
254 max_in: u32,
255 max_out: u32,
256 resident_bytes: u64,
257}
258
259impl TernaryFfnResident {
260 pub fn build(
265 device: &wgpu::Device,
266 queue: &wgpu::Queue,
267 tensors: &[(u64, usize, usize, &[u8])],
268 ) -> Option<Self> {
269 if tensors.is_empty() {
270 return None;
271 }
272 let mut map: HashMap<u64, ResidentTernaryTensor> = HashMap::with_capacity(tensors.len());
274 let mut uploads: Vec<(u64, Vec<u8>)> = Vec::with_capacity(tensors.len());
275 let mut cursor: u64 = 0;
276 let (mut max_in, mut max_out) = (0u32, 0u32);
277 for &(key, n_in, n_out, blob) in tensors {
278 let count = n_in.checked_mul(n_out)?;
279 if count == 0 {
280 return None;
281 }
282 let (scale, mut packed) = rebake_ternary_blob_to_2bit(blob, count);
283 if packed.is_empty() {
284 return None;
285 }
286 while packed.len() % 4 != 0 {
288 packed.push(0);
289 }
290 let gpu_offset = align_up_u64(cursor, TERNARY_RESIDENT_ALIGN);
291 let packed_len = packed.len() as u32;
292 map.insert(
293 key,
294 ResidentTernaryTensor {
295 gpu_offset,
296 packed_len,
297 n_in: n_in as u32,
298 n_out: n_out as u32,
299 scale,
300 },
301 );
302 uploads.push((gpu_offset, packed));
303 cursor = gpu_offset + packed_len as u64;
304 max_in = max_in.max(n_in as u32);
305 max_out = max_out.max(n_out as u32);
306 }
307 let resident_bytes = cursor.max(4);
308
309 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
311 label: Some("ternary_ffn_2bit"),
312 source: wgpu::ShaderSource::Wgsl(TERNARY_GEMM_2BIT_WGSL.into()),
313 });
314 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
315 label: Some("ternary_ffn_resident_pipeline"),
316 layout: None,
317 module: &shader,
318 entry_point: Some("ternary_gemm"),
319 compilation_options: Default::default(),
320 cache: None,
321 });
322
323 let weights = device.create_buffer(&wgpu::BufferDescriptor {
325 label: Some("TernaryFfnResidentWeights"),
326 size: resident_bytes,
327 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
328 mapped_at_creation: false,
329 });
330 for (off, bytes) in &uploads {
331 queue.write_buffer(&weights, *off, bytes);
332 }
333 let act_buf = device.create_buffer(&wgpu::BufferDescriptor {
334 label: Some("TernaryFfnAct"),
335 size: (max_in as u64 * 4).max(4),
336 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
337 mapped_at_creation: false,
338 });
339 let out_buf = device.create_buffer(&wgpu::BufferDescriptor {
340 label: Some("TernaryFfnOut"),
341 size: (max_out as u64 * 4).max(4),
342 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
343 mapped_at_creation: false,
344 });
345 let params_buf = device.create_buffer(&wgpu::BufferDescriptor {
346 label: Some("TernaryFfnParams"),
347 size: 32,
348 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
349 mapped_at_creation: false,
350 });
351 let staging = device.create_buffer(&wgpu::BufferDescriptor {
352 label: Some("TernaryFfnStaging"),
353 size: (max_out as u64 * 4).max(4),
354 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
355 mapped_at_creation: false,
356 });
357
358 Some(Self {
359 pipeline,
360 weights,
361 act_buf,
362 out_buf,
363 params_buf,
364 staging,
365 map,
366 max_in,
367 max_out,
368 resident_bytes,
369 })
370 }
371
372 pub fn contains(&self, key: u64) -> bool {
374 self.map.contains_key(&key)
375 }
376
377 pub fn resident_bytes(&self) -> u64 {
379 self.resident_bytes
380 }
381
382 pub fn len(&self) -> usize {
384 self.map.len()
385 }
386
387 pub fn is_empty(&self) -> bool {
388 self.map.is_empty()
389 }
390
391 pub fn gemv(
395 &self,
396 device: &wgpu::Device,
397 queue: &wgpu::Queue,
398 key: u64,
399 input: &[f32],
400 out: &mut [f32],
401 n_in: usize,
402 n_out: usize,
403 ) -> bool {
404 let t = match self.map.get(&key) {
405 Some(t) => *t,
406 None => return false,
407 };
408 if t.n_in as usize != n_in
409 || t.n_out as usize != n_out
410 || n_in > input.len()
411 || n_out > out.len()
412 || n_in as u32 > self.max_in
413 || n_out as u32 > self.max_out
414 {
415 return false;
416 }
417
418 queue.write_buffer(&self.act_buf, 0, bytemuck::cast_slice(&input[..n_in]));
419 let params = ternary_params_bytes(n_in as u32, n_out as u32, 1, t.scale);
420 queue.write_buffer(&self.params_buf, 0, ¶ms);
421
422 let weight_binding = wgpu::BindingResource::Buffer(wgpu::BufferBinding {
423 buffer: &self.weights,
424 offset: t.gpu_offset,
425 size: std::num::NonZeroU64::new(t.packed_len as u64),
426 });
427 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
428 label: Some("ternary_ffn_resident_bind"),
429 layout: &self.pipeline.get_bind_group_layout(0),
430 entries: &[
431 wgpu::BindGroupEntry {
432 binding: 0,
433 resource: self.act_buf.as_entire_binding(),
434 },
435 wgpu::BindGroupEntry {
436 binding: 1,
437 resource: weight_binding,
438 },
439 wgpu::BindGroupEntry {
440 binding: 2,
441 resource: self.params_buf.as_entire_binding(),
442 },
443 wgpu::BindGroupEntry {
444 binding: 3,
445 resource: self.out_buf.as_entire_binding(),
446 },
447 ],
448 });
449
450 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
451 label: Some("ternary_ffn_enc"),
452 });
453 {
454 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
455 label: Some("ternary_ffn_pass"),
456 timestamp_writes: None,
457 });
458 pass.set_pipeline(&self.pipeline);
459 pass.set_bind_group(0, &bind, &[]);
460 pass.dispatch_workgroups((n_out as u32).div_ceil(64).max(1), 1, 1);
461 }
462 let out_bytes = (n_out * 4) as wgpu::BufferAddress;
463 encoder.copy_buffer_to_buffer(&self.out_buf, 0, &self.staging, 0, out_bytes);
464 queue.submit(Some(encoder.finish()));
465
466 let slice = self.staging.slice(..out_bytes);
467 let (tx, rx) = std::sync::mpsc::channel();
468 slice.map_async(wgpu::MapMode::Read, move |r| {
469 let _ = tx.send(r);
470 });
471 let _ = device.poll(wgpu::PollType::wait_indefinitely());
472 match rx.recv() {
473 Ok(Ok(())) => {}
474 _ => return false,
475 }
476 let data = slice
477 .get_mapped_range()
478 .expect("wgpu buffer map_range failed");
479 out[..n_out].copy_from_slice(&bytemuck::cast_slice(&data)[..n_out]);
480 drop(data);
481 self.staging.unmap();
482 true
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::ternary::{pack_trits, ternary_gemm_cpu};
490
491 fn try_gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
492 let instance = wgpu::Instance::default();
493 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
494 power_preference: wgpu::PowerPreference::HighPerformance,
495 ..Default::default()
496 }))
497 .ok()?;
498 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
499 }
500
501 #[test]
504 fn ternary_gemm_gpu_matches_cpu_oracle() {
505 let Some((device, queue)) = try_gpu() else {
506 eprintln!("ternary_gemm_gpu: no wgpu adapter — skipping on-device parity");
507 return;
508 };
509
510 let (n_in, n_out, n_batch) = (7usize, 5usize, 3usize);
512 let scale = 0.37_f32;
513 let trits: Vec<i8> = (0..n_in * n_out).map(|k| (k % 3) as i8 - 1).collect();
514 let packed = pack_trits(&trits);
515 let act: Vec<f32> = (0..n_in * n_batch)
516 .map(|j| (j as f32) * 0.25 - 1.5)
517 .collect();
518
519 let gpu = ternary_gemm_gpu(&device, &queue, &act, &packed, scale, n_in, n_out, n_batch);
520
521 let mut cpu = vec![0.0f32; n_batch * n_out];
522 ternary_gemm_cpu(&act, &packed, scale, n_in, n_out, n_batch, 0, 0, &mut cpu);
523
524 assert_eq!(gpu.len(), cpu.len());
525 for i in 0..cpu.len() {
526 assert!(
527 (gpu[i] - cpu[i]).abs() < 1e-4,
528 "elem {i}: gpu {} vs cpu {}",
529 gpu[i],
530 cpu[i]
531 );
532 }
533 eprintln!(
534 "ternary_gemm_gpu: on-device parity OK ({} elems)",
535 cpu.len()
536 );
537 }
538
539 #[test]
541 fn ternary_gemm_gpu_2bit_matches_cpu_oracle() {
542 use crate::ternary::{pack_trits_2bit, ternary_gemm_cpu_2bit};
543 let Some((device, queue)) = try_gpu() else {
544 eprintln!("ternary_gemm_gpu_2bit: no wgpu adapter — skipping");
545 return;
546 };
547 let (n_in, n_out, n_batch) = (7usize, 5usize, 3usize);
548 let scale = 0.37_f32;
549 let trits: Vec<i8> = (0..n_in * n_out).map(|k| (k % 3) as i8 - 1).collect();
550 let packed = pack_trits_2bit(&trits);
551 let act: Vec<f32> = (0..n_in * n_batch)
552 .map(|j| (j as f32) * 0.25 - 1.5)
553 .collect();
554
555 let gpu =
556 ternary_gemm_gpu_2bit(&device, &queue, &act, &packed, scale, n_in, n_out, n_batch);
557 let mut cpu = vec![0.0f32; n_batch * n_out];
558 ternary_gemm_cpu_2bit(&act, &packed, scale, n_in, n_out, n_batch, 0, 0, &mut cpu);
559 for i in 0..cpu.len() {
560 assert!(
561 (gpu[i] - cpu[i]).abs() < 1e-4,
562 "elem {i}: gpu {} vs cpu {}",
563 gpu[i],
564 cpu[i]
565 );
566 }
567 eprintln!(
568 "ternary_gemm_gpu_2bit: on-device parity OK ({} elems)",
569 cpu.len()
570 );
571 }
572
573 fn base3_blob(scale: f32, trits: &[i8]) -> Vec<u8> {
575 let mut b = scale.to_le_bytes().to_vec();
576 b.extend_from_slice(&pack_trits(trits));
577 b
578 }
579
580 #[test]
585 fn ternary_ffn_resident_matches_cpu_oracle() {
586 let Some((device, queue)) = try_gpu() else {
587 eprintln!("ternary_ffn_resident: no wgpu adapter — skipping");
588 return;
589 };
590 let a = (0x1000u64, 64usize, 40usize, 0.30f32);
592 let b = (0x2000u64, 96usize, 24usize, 0.11f32);
593 let trits_a: Vec<i8> = (0..a.1 * a.2).map(|k| (k % 3) as i8 - 1).collect();
594 let trits_b: Vec<i8> = (0..b.1 * b.2)
595 .map(|k| ((k * 7 + 2) % 3) as i8 - 1)
596 .collect();
597 let blob_a = base3_blob(a.3, &trits_a);
598 let blob_b = base3_blob(b.3, &trits_b);
599
600 let resident = TernaryFfnResident::build(
601 &device,
602 &queue,
603 &[(a.0, a.1, a.2, &blob_a), (b.0, b.1, b.2, &blob_b)],
604 )
605 .expect("build resident ternary set");
606 assert_eq!(resident.len(), 2);
607 assert!(resident.contains(a.0) && resident.contains(b.0));
608
609 for (key, n_in, n_out, scale, blob) in
610 [(a.0, a.1, a.2, a.3, &blob_a), (b.0, b.1, b.2, b.3, &blob_b)]
611 {
612 let act: Vec<f32> = (0..n_in).map(|j| (j as f32) * 0.25 - 1.0).collect();
613 let mut gpu = vec![0f32; n_out];
614 assert!(
615 resident.gemv(&device, &queue, key, &act, &mut gpu, n_in, n_out),
616 "resident gemv must succeed for key {key:#x}"
617 );
618 let mut cpu = vec![0f32; n_out];
619 ternary_gemm_cpu(&act, &blob[4..], scale, n_in, n_out, 1, 0, 0, &mut cpu);
620 for i in 0..n_out {
621 assert!(
622 (gpu[i] - cpu[i]).abs() < 1e-4,
623 "key {key:#x} row {i}: gpu {} vs cpu {}",
624 gpu[i],
625 cpu[i]
626 );
627 }
628 }
629 let mut tmp = vec![0f32; 64];
631 assert!(
632 !resident.gemv(&device, &queue, 0xDEAD, &[0.0; 64], &mut tmp, 64, 40),
633 "absent key must fail-closed"
634 );
635 assert!(
636 !resident.gemv(&device, &queue, a.0, &[0.0; 64], &mut tmp, 64, 64),
637 "present key with mismatched n_out must fail-closed (a is 64x40, not 64x64)"
638 );
639 eprintln!(
640 "ternary_ffn_resident: on-device parity OK (2 tensors, {} resident bytes)",
641 resident.resident_bytes()
642 );
643 }
644
645 #[test]
649 fn ternary_gemm_2bit_vs_base3_indicative_timing() {
650 use crate::ternary::{pack_trits, pack_trits_2bit};
651 use std::time::Instant;
652 let Some((device, queue)) = try_gpu() else {
653 eprintln!("ternary timing: no wgpu adapter — skipping");
654 return;
655 };
656 let (n_in, n_out, n_batch) = (4096usize, 4096usize, 1usize); let scale = 0.05_f32;
658 let trits: Vec<i8> = (0..n_in * n_out)
659 .map(|k| ((k * 7 + 1) % 3) as i8 - 1)
660 .collect();
661 let p3 = pack_trits(&trits);
662 let p2 = pack_trits_2bit(&trits);
663 let act: Vec<f32> = (0..n_in).map(|j| (j as f32 % 13.0) * 0.1 - 0.6).collect();
664 let iters = 60;
665
666 let _ = ternary_gemm_gpu(&device, &queue, &act, &p3, scale, n_in, n_out, n_batch);
668 let _ = ternary_gemm_gpu_2bit(&device, &queue, &act, &p2, scale, n_in, n_out, n_batch);
669
670 let t0 = Instant::now();
671 for _ in 0..iters {
672 let _ = ternary_gemm_gpu(&device, &queue, &act, &p3, scale, n_in, n_out, n_batch);
673 }
674 let base3 = t0.elapsed().as_secs_f64() / iters as f64 * 1e3;
675
676 let t1 = Instant::now();
677 for _ in 0..iters {
678 let _ = ternary_gemm_gpu_2bit(&device, &queue, &act, &p2, scale, n_in, n_out, n_batch);
679 }
680 let bit2 = t1.elapsed().as_secs_f64() / iters as f64 * 1e3;
681
682 eprintln!(
683 "ternary GEMV {}x{} (indicative, incl. overhead): base-3 branchy {:.3} ms/iter | 2-bit branchless {:.3} ms/iter | speedup {:.2}x",
684 n_out, n_in, base3, bit2, base3 / bit2.max(1e-9)
685 );
686 }
687
688 const F16_GEMV_WGSL: &str = include_str!("../shaders/f16_gemv.wgsl");
690
691 fn bench_kernel(
696 device: &wgpu::Device,
697 queue: &wgpu::Queue,
698 wgsl: &str,
699 entry: &str,
700 weight_bytes: &[u8],
701 n_in: usize,
702 n_out: usize,
703 ) -> f64 {
704 use std::time::Instant;
705 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
706 label: Some("bench"),
707 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
708 });
709 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
710 label: Some("bench_pipeline"),
711 layout: None,
712 module: &shader,
713 entry_point: Some(entry),
714 compilation_options: Default::default(),
715 cache: None,
716 });
717 let mk = |contents: &[u8], usage: wgpu::BufferUsages| {
718 let b = device.create_buffer(&wgpu::BufferDescriptor {
719 label: None,
720 size: contents.len().max(4) as u64,
721 usage,
722 mapped_at_creation: false,
723 });
724 if !contents.is_empty() {
725 queue.write_buffer(&b, 0, contents);
726 }
727 b
728 };
729 let act = mk(
730 bytemuck::cast_slice(&vec![0.1f32; n_in]),
731 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
732 );
733 let mut w = weight_bytes.to_vec();
734 while w.len() % 4 != 0 || w.is_empty() {
735 w.push(0);
736 }
737 let wbuf = mk(
738 &w,
739 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
740 );
741 let params = super::ternary_params_bytes(n_in as u32, n_out as u32, 1, 1.0);
742 let pbuf = mk(
743 ¶ms,
744 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
745 );
746 let obuf = device.create_buffer(&wgpu::BufferDescriptor {
747 label: None,
748 size: (n_out * 4).max(4) as u64,
749 usage: wgpu::BufferUsages::STORAGE,
750 mapped_at_creation: false,
751 });
752 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
753 label: None,
754 layout: &pipeline.get_bind_group_layout(0),
755 entries: &[
756 wgpu::BindGroupEntry {
757 binding: 0,
758 resource: act.as_entire_binding(),
759 },
760 wgpu::BindGroupEntry {
761 binding: 1,
762 resource: wbuf.as_entire_binding(),
763 },
764 wgpu::BindGroupEntry {
765 binding: 2,
766 resource: pbuf.as_entire_binding(),
767 },
768 wgpu::BindGroupEntry {
769 binding: 3,
770 resource: obuf.as_entire_binding(),
771 },
772 ],
773 });
774 let wg_x = (n_out as u32).div_ceil(64).max(1);
775 let (k, s) = (32u32, 8u32);
776 let submit_batch = || {
777 let mut enc =
778 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
779 {
780 let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
781 label: None,
782 timestamp_writes: None,
783 });
784 pass.set_pipeline(&pipeline);
785 pass.set_bind_group(0, &bind, &[]);
786 for _ in 0..k {
787 pass.dispatch_workgroups(wg_x, 1, 1);
788 }
789 }
790 queue.submit(Some(enc.finish()));
791 let _ = device.poll(wgpu::PollType::wait_indefinitely());
792 };
793 submit_batch(); let t0 = Instant::now();
795 for _ in 0..s {
796 submit_batch();
797 }
798 t0.elapsed().as_secs_f64() * 1e3 / (k * s) as f64
799 }
800
801 #[test]
804 fn ternary_kernel_benchmark() {
805 use crate::ternary::{pack_trits, pack_trits_2bit};
806 let Some((device, queue)) = try_gpu() else {
807 eprintln!("ternary_kernel_benchmark: no wgpu adapter — skipping");
808 return;
809 };
810 let (n_in, n_out) = (4096usize, 4096usize); let trits: Vec<i8> = (0..n_in * n_out)
812 .map(|k| ((k * 7 + 1) % 3) as i8 - 1)
813 .collect();
814 let p3 = pack_trits(&trits);
815 let p2 = pack_trits_2bit(&trits);
816 let f16 = vec![0u8; n_in * n_out * 2]; let f16ms = bench_kernel(
819 &device,
820 &queue,
821 F16_GEMV_WGSL,
822 "f16_gemv",
823 &f16,
824 n_in,
825 n_out,
826 );
827 let base3 = bench_kernel(
828 &device,
829 &queue,
830 TERNARY_GEMM_WGSL,
831 "ternary_gemm",
832 &p3,
833 n_in,
834 n_out,
835 );
836 let bit2 = bench_kernel(
837 &device,
838 &queue,
839 TERNARY_GEMM_2BIT_WGSL,
840 "ternary_gemm",
841 &p2,
842 n_in,
843 n_out,
844 );
845
846 eprintln!(
847 "── A2000 GEMV {}x{} batch=1 (persistent pipeline, {}MB/{}KB/{}KB weights) ──",
848 n_out,
849 n_in,
850 f16.len() / (1 << 20),
851 p3.len() >> 10,
852 p2.len() >> 10
853 );
854 eprintln!(" F16 baseline : {:.4} ms/dispatch", f16ms);
855 eprintln!(
856 " ternary base-3 : {:.4} ms/dispatch ({:.2}x vs F16)",
857 base3,
858 f16ms / base3.max(1e-12)
859 );
860 eprintln!(
861 " ternary 2-bit branchless: {:.4} ms/dispatch ({:.2}x vs F16, {:.2}x vs base-3)",
862 bit2,
863 f16ms / bit2.max(1e-12),
864 base3 / bit2.max(1e-12)
865 );
866 }
867}