1use bytemuck::{Pod, Zeroable};
4
5#[cfg(not(target_arch = "wasm32"))]
6use super::resident_substrate::{global_resident_substrate, MAX_KNN_HITS, MAX_RESIDENT_NODES};
7use super::Tensor10D;
8
9pub const TENSOR_VOLUME_STRIDE_FLOATS: u32 = 10;
10
11#[repr(C)]
12#[derive(Clone, Copy, Pod, Zeroable)]
13struct VolumeGpuParams {
14 node_count: u32,
15 max_distance: f32,
16 stride_floats: u32,
17 max_hits: u32,
18}
19
20#[cfg(not(target_arch = "wasm32"))]
21pub struct TensorVolumeGpu {
22 pipeline: wgpu::ComputePipeline,
23 query_buf: wgpu::Buffer,
24 nodes_buf: wgpu::Buffer,
25 params_buf: wgpu::Buffer,
26 hits_buf: wgpu::Buffer,
27 count_buf: wgpu::Buffer,
28 staging_hits: wgpu::Buffer,
29 staging_count: wgpu::Buffer,
30 max_nodes: u32,
31}
32
33#[cfg(not(target_arch = "wasm32"))]
34impl TensorVolumeGpu {
35 pub fn try_new(device: &wgpu::Device) -> Option<Self> {
36 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
37 label: Some("TensorVolumeShader"),
38 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/tensor_volume.wgsl").into()),
39 });
40 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
41 label: Some("TensorVolumePipeline"),
42 layout: None,
43 module: &shader,
44 entry_point: Some("main"),
45 compilation_options: Default::default(),
46 cache: None,
47 });
48 let max_nodes = MAX_RESIDENT_NODES as u32;
49 let node_floats = max_nodes * TENSOR_VOLUME_STRIDE_FLOATS;
50 Some(Self {
51 pipeline,
52 query_buf: device.create_buffer(&wgpu::BufferDescriptor {
53 label: Some("TensorVolumeQuery"),
54 size: std::mem::size_of::<Tensor10D>() as wgpu::BufferAddress,
55 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
56 mapped_at_creation: false,
57 }),
58 nodes_buf: device.create_buffer(&wgpu::BufferDescriptor {
59 label: Some("TensorVolumeNodes"),
60 size: (node_floats as usize * 4) as wgpu::BufferAddress,
61 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
62 mapped_at_creation: false,
63 }),
64 params_buf: device.create_buffer(&wgpu::BufferDescriptor {
65 label: Some("TensorVolumeParams"),
66 size: std::mem::size_of::<VolumeGpuParams>() as wgpu::BufferAddress,
67 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
68 mapped_at_creation: false,
69 }),
70 hits_buf: device.create_buffer(&wgpu::BufferDescriptor {
71 label: Some("TensorVolumeHits"),
72 size: (MAX_KNN_HITS * 4) as wgpu::BufferAddress,
73 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
74 mapped_at_creation: false,
75 }),
76 count_buf: device.create_buffer(&wgpu::BufferDescriptor {
77 label: Some("TensorVolumeHitCount"),
78 size: 4,
79 usage: wgpu::BufferUsages::STORAGE
82 | wgpu::BufferUsages::COPY_SRC
83 | wgpu::BufferUsages::COPY_DST,
84 mapped_at_creation: false,
85 }),
86 staging_hits: device.create_buffer(&wgpu::BufferDescriptor {
87 label: Some("TensorVolumeHitsStaging"),
88 size: (MAX_KNN_HITS * 4) as wgpu::BufferAddress,
89 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
90 mapped_at_creation: false,
91 }),
92 staging_count: device.create_buffer(&wgpu::BufferDescriptor {
93 label: Some("TensorVolumeCountStaging"),
94 size: 4,
95 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
96 mapped_at_creation: false,
97 }),
98 max_nodes,
99 })
100 }
101
102 fn upload_nodes(&self, queue: &wgpu::Queue, node_count: u32) -> bool {
103 let substrate = global_resident_substrate();
104 let count = node_count.min(substrate.node_count()).min(self.max_nodes);
105 if count == 0 {
106 return false;
107 }
108 let mut flat = [0f32; MAX_RESIDENT_NODES * TENSOR_VOLUME_STRIDE_FLOATS as usize];
109 for i in 0..count as usize {
110 if let Some(t) = substrate.tensor_at(i as u32) {
111 let base = i * TENSOR_VOLUME_STRIDE_FLOATS as usize;
112 flat[base] = t.q;
113 flat[base + 1] = t.v;
114 flat[base + 2] = t.w;
115 flat[base + 3] = t.x;
116 flat[base + 4] = t.y;
117 flat[base + 5] = t.z;
118 flat[base + 6] = t.t;
119 flat[base + 7] = t.alpha;
120 flat[base + 8] = t.mu;
121 flat[base + 9] = t.sigma;
122 }
123 }
124 let bytes =
125 (count as usize * TENSOR_VOLUME_STRIDE_FLOATS as usize * 4) as wgpu::BufferAddress;
126 queue.write_buffer(
127 &self.nodes_buf,
128 0,
129 bytemuck::cast_slice(&flat[..count as usize * 10]),
130 );
131 let _ = bytes;
132 true
133 }
134
135 pub fn tensor_search_into(
137 &self,
138 device: &wgpu::Device,
139 queue: &wgpu::Queue,
140 query: &Tensor10D,
141 max_distance: f32,
142 out: &mut [usize],
143 ) -> usize {
144 let node_count = global_resident_substrate().node_count().min(self.max_nodes);
145 if node_count == 0 || out.is_empty() || !self.upload_nodes(queue, node_count) {
146 return 0;
147 }
148
149 queue.write_buffer(&self.query_buf, 0, bytemuck::bytes_of(query));
150 let params = VolumeGpuParams {
151 node_count,
152 max_distance,
153 stride_floats: TENSOR_VOLUME_STRIDE_FLOATS,
154 max_hits: MAX_KNN_HITS as u32,
155 };
156 queue.write_buffer(&self.params_buf, 0, bytemuck::bytes_of(¶ms));
157 queue.write_buffer(&self.count_buf, 0, &[0u8; 4]);
158
159 let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
160 label: Some("TensorVolumeBindGroup"),
161 layout: &self.pipeline.get_bind_group_layout(0),
162 entries: &[
163 wgpu::BindGroupEntry {
164 binding: 0,
165 resource: self.query_buf.as_entire_binding(),
166 },
167 wgpu::BindGroupEntry {
168 binding: 1,
169 resource: self.nodes_buf.as_entire_binding(),
170 },
171 wgpu::BindGroupEntry {
172 binding: 2,
173 resource: self.params_buf.as_entire_binding(),
174 },
175 wgpu::BindGroupEntry {
176 binding: 3,
177 resource: self.hits_buf.as_entire_binding(),
178 },
179 wgpu::BindGroupEntry {
180 binding: 4,
181 resource: self.count_buf.as_entire_binding(),
182 },
183 ],
184 });
185
186 let wg = (node_count + 63) / 64;
187 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
188 label: Some("TensorVolumeEncoder"),
189 });
190 {
191 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
192 label: Some("TensorVolumePass"),
193 timestamp_writes: None,
194 });
195 pass.set_pipeline(&self.pipeline);
196 pass.set_bind_group(0, &bind, &[]);
197 pass.dispatch_workgroups(wg, 1, 1);
198 }
199 encoder.copy_buffer_to_buffer(
200 &self.hits_buf,
201 0,
202 &self.staging_hits,
203 0,
204 (MAX_KNN_HITS * 4) as u64,
205 );
206 encoder.copy_buffer_to_buffer(&self.count_buf, 0, &self.staging_count, 0, 4);
207 queue.submit(Some(encoder.finish()));
208
209 let _ = device.poll(wgpu::PollType::wait_indefinitely());
210 let count_slice = self.staging_count.slice(..4);
211 let (tx, rx) = std::sync::mpsc::channel();
212 count_slice.map_async(wgpu::MapMode::Read, move |r| {
213 let _ = tx.send(r);
214 });
215 let _ = device.poll(wgpu::PollType::wait_indefinitely());
216 if rx.recv().ok().and_then(|r| r.ok()).is_none() {
217 return 0;
218 }
219 let count_data = count_slice
220 .get_mapped_range()
221 .expect("wgpu buffer map_range failed");
222 let total = u32::from_le_bytes(count_data[..4].try_into().unwrap_or([0; 4])) as usize;
223 drop(count_data);
224 self.staging_count.unmap();
225
226 let hits_slice = self.staging_hits.slice(..(MAX_KNN_HITS * 4) as u64);
227 let (tx2, rx2) = std::sync::mpsc::channel();
228 hits_slice.map_async(wgpu::MapMode::Read, move |r| {
229 let _ = tx2.send(r);
230 });
231 let _ = device.poll(wgpu::PollType::wait_indefinitely());
232 if rx2.recv().ok().and_then(|r| r.ok()).is_none() {
233 return 0;
234 }
235 let hits_data = hits_slice
236 .get_mapped_range()
237 .expect("wgpu buffer map_range failed");
238 let indices: &[u32] = bytemuck::cast_slice(&hits_data);
239 let n = total.min(out.len()).min(indices.len());
240 for i in 0..n {
241 out[i] = indices[i] as usize;
242 }
243 drop(hits_data);
244 self.staging_hits.unmap();
245 n
246 }
247}
248
249#[cfg(not(target_arch = "wasm32"))]
250static VOLUME_GPU: std::sync::OnceLock<Option<TensorVolumeGpu>> = std::sync::OnceLock::new();
251
252#[cfg(not(target_arch = "wasm32"))]
254pub fn try_gpu_tensor_search_into(
255 query: &Tensor10D,
256 max_distance: f32,
257 out: &mut [usize],
258) -> Option<usize> {
259 let gpu_ctx = crate::gpu_context::shared_gpu();
260 let vol = VOLUME_GPU.get_or_init(|| TensorVolumeGpu::try_new(&gpu_ctx.device));
261 let vol = vol.as_ref()?;
262 let n = vol.tensor_search_into(
263 &gpu_ctx.device,
264 crate::gpu_context::shared_gpu()
265 .queue_for_universe(crate::gpu_context::ComputeUniverse::Tensor10D),
266 query,
267 max_distance,
268 out,
269 );
270 if n > 0 {
271 Some(n)
272 } else {
273 None
274 }
275}
276
277#[cfg(target_arch = "wasm32")]
278pub fn try_gpu_tensor_search_into(
279 _query: &Tensor10D,
280 _max_distance: f32,
281 _out: &mut [usize],
282) -> Option<usize> {
283 None
284}
285
286pub fn cpu_tensor_search_into(
298 query: &Tensor10D,
299 nodes: &[Tensor10D],
300 max_distance: f32,
301 out: &mut [usize],
302) -> usize {
303 let mut matches = 0usize;
304 for (idx, node) in nodes.iter().enumerate() {
305 if query.full_distance(node) <= max_distance {
306 if matches < out.len() {
307 out[matches] = idx;
308 }
309 matches += 1;
310 }
311 }
312 matches
313}
314
315#[cfg(test)]
316mod cpu_reference_tests {
317 use super::*;
318
319 #[test]
320 fn cpu_tensor_search_matches_metric() {
321 let zeros = Tensor10D::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
324 let nodes = [
325 zeros, Tensor10D::new(0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), Tensor10D::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0), Tensor10D::new(9.0, 9.0, 9.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), ];
331 let mut out = [usize::MAX; 8];
332
333 let n = cpu_tensor_search_into(&zeros, &nodes, 1.5, &mut out);
335 assert_eq!(n, 3);
336 let hits: std::collections::BTreeSet<usize> = out[..n].iter().copied().collect();
337 assert_eq!(hits, [0, 1, 3].into_iter().collect());
338
339 assert_eq!(cpu_tensor_search_into(&zeros, &nodes, 5.0, &mut out), 4);
341 assert_eq!(cpu_tensor_search_into(&zeros, &nodes, 0.0, &mut out), 2);
343 }
344
345 #[test]
346 fn cpu_tensor_search_honors_topology_class() {
347 let query = Tensor10D::new(0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
351 let near_wrap = Tensor10D::new(0.0, 1.0, 0.0, 0.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
352 let mut out = [usize::MAX; 4];
353 assert_eq!(
355 cpu_tensor_search_into(&query, &[near_wrap], 0.2, &mut out),
356 1,
357 "cyclic metric must wrap x; node should be within 0.2"
358 );
359 let euclid_query = Tensor10D::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
361 let euclid_node = Tensor10D::new(0.0, 0.0, 0.0, 0.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
362 assert_eq!(
363 cpu_tensor_search_into(&euclid_query, &[euclid_node], 0.2, &mut out),
364 0
365 );
366 }
367}