Skip to main content

ab_proof_of_space_gpu/shader/
find_proofs.rs

1use crate::shader::MIN_SUBGROUP_SIZE;
2use crate::shader::constants::{
3    K, NUM_MATCH_BUCKETS, NUM_S_BUCKETS, NUM_TABLES, REDUCED_MATCHES_COUNT,
4};
5use crate::shader::find_matches_and_compute_f7::{NUM_ELEMENTS_PER_S_BUCKET, ProofTargets};
6use crate::shader::types::{Position, PositionExt};
7use core::mem::MaybeUninit;
8use spirv_std::arch::{
9    atomic_or, subgroup_ballot, subgroup_memory_barrier, subgroup_shuffle, subgroup_u_min,
10    workgroup_memory_barrier_with_group_sync,
11};
12use spirv_std::glam::UVec3;
13use spirv_std::memory::{Scope, Semantics};
14use spirv_std::spirv;
15#[cfg(not(target_arch = "spirv"))]
16use subspace_core_primitives::pos::PosProof;
17
18// TODO: Same number as hardcoded in `#[spirv(compute(threads(..)))]` below, can be removed once
19//  https://github.com/Rust-GPU/rust-gpu/discussions/287 is resolved
20pub const WORKGROUP_SIZE: u32 = 256;
21const PROOF_X_SOURCES: usize = 2usize.pow(NUM_TABLES as u32 - 1);
22const PROOF_BITS: usize = PROOF_X_SOURCES * K as usize;
23const PROOF_BYTES: usize = PROOF_BITS.div_ceil(u8::BITS as usize);
24pub const PROOF_U32_WORDS: usize = PROOF_BYTES.div_ceil(size_of::<u32>());
25pub const FOUND_PROOFS_U32_WORDS: usize = {
26    assert!(NUM_S_BUCKETS.is_multiple_of(u32::BITS as usize));
27
28    NUM_S_BUCKETS / u32::BITS as usize
29};
30
31#[derive(Debug, Copy, Clone)]
32#[repr(C)]
33pub struct Proofs {
34    found_proofs: [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
35    // TODO: Calculate bit mask for proofs found upfront and reduce the size here to just
36    //  `NUM_CHUNKS`
37    proofs: [MaybeUninit<u32>; PROOF_U32_WORDS * NUM_S_BUCKETS],
38}
39
40// This is equivalent to the above but used for interpretation by the host
41#[derive(Debug, Copy, Clone)]
42#[cfg(not(target_arch = "spirv"))]
43#[repr(C)]
44pub struct ProofsHost {
45    // TODO: Would have been nice to avoid filtering-out on the host
46    /// S-buckets at which proofs were found, there will be more than `Record::NUM_CHUNKS` proofs
47    /// here, needs to be filtered-out by the host
48    pub found_proofs: [u8; NUM_S_BUCKETS / u8::BITS as usize],
49    // TODO: Calculate bit mask for proofs found upfront and reduce the size here to just
50    //  `NUM_CHUNKS`
51    /// All proofs, those that correspond to set bits of `found_proofs` exist
52    pub proofs: [PosProof; NUM_S_BUCKETS],
53}
54
55#[cfg(not(target_arch = "spirv"))]
56const _: () = {
57    assert!(size_of::<Proofs>() == size_of::<ProofsHost>());
58};
59
60// TODO: Optimize this for various cases like when all buckets fit into subgroup size, when subgroup
61//  size is large enough to process multiple buckets at once (especially since buckets often are
62//  less than 16 elements, meaning AMD GPUs can process 4 at once) with clustered subgroup
63//  operations, etc.
64// TODO: Make unsafe and avoid bounds check
65#[expect(
66    clippy::too_many_arguments,
67    reason = "Both I/O and Vulkan stuff together take a lot of arguments"
68)]
69#[inline(always)]
70fn find_local_proof_targets<const SUBGROUP_SIZE: u32>(
71    local_invocation_id: u32,
72    subgroup_id: u32,
73    subgroup_local_invocation_id: u32,
74    positions_group_index: u32,
75    bucket_sizes: &mut [u32; NUM_S_BUCKETS],
76    buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
77    found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
78    found_proofs_scratch: &mut [MaybeUninit<u32>; (WORKGROUP_SIZE / u32::BITS) as usize],
79) -> [Position; 2] {
80    let local_invocation_id = local_invocation_id as usize;
81    let base = positions_group_index * SUBGROUP_SIZE;
82
83    let mut min = [Position::SENTINEL; 2];
84
85    let local_bucket_size = {
86        let bucket_id = base + subgroup_local_invocation_id;
87
88        let local_bucket_size = bucket_sizes[bucket_id as usize];
89        bucket_sizes[bucket_id as usize] = 0;
90
91        local_bucket_size
92    };
93
94    for local_bucket_id in 0..SUBGROUP_SIZE {
95        let bucket_id = (base + local_bucket_id) as usize;
96        let bucket_size = subgroup_shuffle(local_bucket_size, local_bucket_id);
97        let bucket = &buckets[bucket_id];
98
99        // TODO: Can't use the struct due to this issue in Naga:
100        //  https://github.com/gfx-rs/wgpu/issues/8389#issuecomment-3430788603
101        // let mut local_min = ProofTargets {
102        //     absolute_position: u32::MAX,
103        //     positions: [Position::SENTINEL; 2],
104        // };
105        let mut local_min_absolute_position = [u32::MAX];
106        let mut local_min_positions = [Position::SENTINEL; 2];
107
108        for index in (subgroup_local_invocation_id..bucket_size).step_by(SUBGROUP_SIZE as usize) {
109            let proof_targets = bucket[index as usize];
110            if proof_targets.absolute_position < local_min_absolute_position[0] {
111                local_min_absolute_position[0] = proof_targets.absolute_position;
112                local_min_positions = proof_targets.positions;
113            }
114        }
115
116        let min_absolute_position = subgroup_u_min(local_min_absolute_position[0]);
117        let source_lane_mask =
118            subgroup_ballot(local_min_absolute_position[0] == min_absolute_position);
119        // TODO: This intrinsic is not supported by `wgpu` yet:
120        //  https://github.com/gfx-rs/wgpu/issues/8403
121        // let source_lane = subgroup_ballot_find_lsb(source_lane_mask);
122        let source_lane_mask = source_lane_mask.to_array();
123        let mut source_lane = u32::MAX;
124        for i in 0..SUBGROUP_SIZE.div_ceil(u32::BITS) {
125            let word = source_lane_mask[i as usize];
126            if word != 0 {
127                source_lane = word.trailing_zeros() + i * u32::BITS;
128                break;
129            }
130        }
131        let local_min = subgroup_shuffle(local_min_positions, source_lane);
132
133        if subgroup_local_invocation_id == local_bucket_id {
134            min = local_min;
135        }
136    }
137
138    let has_proof = local_bucket_size > 0;
139    let found_proofs_words = subgroup_ballot(has_proof);
140
141    if SUBGROUP_SIZE >= u32::BITS {
142        // For subgroup sizes that are multiple of `u32` words, results can be written directly into
143        // global memory
144        // TODO: should have been `subgroup_elect()`, but it is not implemented in `wgpu` yet:
145        //  https://github.com/gfx-rs/wgpu/issues/5555
146        if subgroup_local_invocation_id == 0 {
147            let start_word = (base / u32::BITS) as usize;
148            found_proofs[start_word].write(found_proofs_words.x);
149
150            if SUBGROUP_SIZE >= 2 * u32::BITS {
151                found_proofs[start_word + 1].write(found_proofs_words.y);
152
153                if SUBGROUP_SIZE >= 4 * u32::BITS {
154                    found_proofs[start_word + 2].write(found_proofs_words.z);
155                    found_proofs[start_word + 3].write(found_proofs_words.w);
156                }
157            }
158        }
159    } else {
160        if local_invocation_id < found_proofs_scratch.len() {
161            found_proofs_scratch[local_invocation_id].write(0);
162        }
163
164        workgroup_memory_barrier_with_group_sync();
165
166        // For subgroups of smaller sizes aggregate results in shared memory first, then write to
167        // global memory
168        // TODO: should have been `subgroup_elect()`, but it is not implemented in `wgpu` yet:
169        //  https://github.com/gfx-rs/wgpu/issues/5555
170        if subgroup_local_invocation_id == 0 {
171            let local_start_bit = subgroup_id * SUBGROUP_SIZE;
172            let local_word_index = (local_start_bit / u32::BITS) as usize;
173            let local_word_shift = local_start_bit % u32::BITS;
174
175            // SAFETY: Initialized above
176            let found_proofs_word =
177                unsafe { found_proofs_scratch[local_word_index].assume_init_mut() };
178            // SAFETY: TODO: Probably should not be unsafe to begin with:
179            //  https://github.com/Rust-GPU/rust-gpu/pull/394#issuecomment-3316594485
180            unsafe {
181                atomic_or::<_, { Scope::Workgroup as u32 }, { Semantics::NONE.bits() }>(
182                    found_proofs_word,
183                    found_proofs_words.x << local_word_shift,
184                );
185            }
186        }
187
188        workgroup_memory_barrier_with_group_sync();
189
190        if local_invocation_id < found_proofs_scratch.len() {
191            let workgroup_base_group_index = positions_group_index - subgroup_id;
192            let workgroup_start_bucket = workgroup_base_group_index * SUBGROUP_SIZE;
193            let global_start_word = (workgroup_start_bucket / u32::BITS) as usize;
194
195            // SAFETY: Initialized above
196            let found_proofs_word =
197                unsafe { found_proofs_scratch[local_invocation_id].assume_init() };
198            found_proofs[global_start_word + local_invocation_id].write(found_proofs_word);
199        }
200    }
201
202    min
203}
204
205// TODO: Make unsafe and avoid bounds check
206#[expect(
207    clippy::too_many_arguments,
208    reason = "Both I/O and Vulkan stuff together take a lot of arguments"
209)]
210fn find_proofs_impl<const SUBGROUP_SIZE: u32>(
211    local_invocation_id: u32,
212    subgroup_id: u32,
213    subgroup_local_invocation_id: u32,
214    positions_group_index: u32,
215    // TODO: This should have been `&[[[Position; 2]; REDUCED_MATCHES_COUNT]; NUM_MATCH_BUCKETS]`,
216    //  but it currently doesn't compile if flattened:
217    //  https://github.com/Rust-GPU/rust-gpu/issues/241#issuecomment-3005693043
218    table_2_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
219    table_3_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
220    table_4_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
221    table_5_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
222    table_6_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
223    bucket_sizes: &mut [u32; NUM_S_BUCKETS],
224    buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET]; NUM_S_BUCKETS],
225    found_proofs: &mut [MaybeUninit<u32>; FOUND_PROOFS_U32_WORDS],
226    // TODO: This should have been `&mut [[MaybeUninit<u32>; PROOF_U32_WORDS]; NUM_S_BUCKETS]`,
227    //  but it currently doesn't compile if flattened:
228    //  https://github.com/Rust-GPU/rust-gpu/issues/241#issuecomment-3005693043
229    proofs: &mut [MaybeUninit<u32>; PROOF_U32_WORDS * NUM_S_BUCKETS],
230    found_proofs_scratch: &mut [MaybeUninit<u32>; (WORKGROUP_SIZE / u32::BITS) as usize],
231) where
232    [(); PROOF_X_SOURCES.div_ceil(SUBGROUP_SIZE as usize)]:,
233    [(); PROOF_U32_WORDS.div_ceil(SUBGROUP_SIZE as usize)]:,
234{
235    let table_6_proof_targets = find_local_proof_targets::<SUBGROUP_SIZE>(
236        local_invocation_id,
237        subgroup_id,
238        subgroup_local_invocation_id,
239        positions_group_index,
240        bucket_sizes,
241        buckets,
242        found_proofs,
243        found_proofs_scratch,
244    );
245
246    // TODO: This proof zeroing will not be needed once proofs are assembled in registers and no
247    //  longer use atomic writes
248    // Zero the proofs range with coalesced writes
249    {
250        let positions_group_words = SUBGROUP_SIZE as usize * PROOF_U32_WORDS;
251        let base_word = positions_group_index as usize * positions_group_words;
252
253        for offset in (subgroup_local_invocation_id as usize..positions_group_words)
254            .step_by(SUBGROUP_SIZE as usize)
255        {
256            proofs[base_word + offset].write(0);
257        }
258
259        subgroup_memory_barrier();
260    }
261
262    // `0` for left `1` for right
263    let left_right = (subgroup_local_invocation_id % 2) as usize;
264    // Otherwise `left_right` will not work as expected
265    const {
266        assert!(MIN_SUBGROUP_SIZE >= 2);
267    }
268
269    // `chunk_index` is used to emulate `for _ in 0..2` loops, while using a single variable for
270    // tracking the progress instead of a separate variable for each loop
271    let mut chunk_index = 0u32;
272    // Reading positions from table 6
273    loop {
274        let table_6_proof_targets = subgroup_shuffle(
275            table_6_proof_targets,
276            SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
277        );
278        let table_6_proof_target = table_6_proof_targets[left_right];
279
280        let table_5_proof_targets = if table_6_proof_target == Position::SENTINEL {
281            [Position::SENTINEL; 2]
282        } else {
283            table_6_positions[table_6_proof_target as usize]
284        };
285
286        // Reading positions from table 5
287        chunk_index <<= 1u8;
288        loop {
289            let table_5_proof_targets = subgroup_shuffle(
290                table_5_proof_targets,
291                SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
292            );
293            let table_5_proof_target = table_5_proof_targets[left_right];
294
295            let table_4_proof_targets = if table_5_proof_target == Position::SENTINEL {
296                [Position::SENTINEL; 2]
297            } else {
298                table_5_positions[table_5_proof_target as usize]
299            };
300
301            // Reading positions from table 4
302            chunk_index <<= 1u8;
303            loop {
304                let table_4_proof_targets = subgroup_shuffle(
305                    table_4_proof_targets,
306                    SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
307                );
308                let table_4_proof_target = table_4_proof_targets[left_right];
309
310                let table_3_proof_targets = if table_4_proof_target == Position::SENTINEL {
311                    [Position::SENTINEL; 2]
312                } else {
313                    table_4_positions[table_4_proof_target as usize]
314                };
315
316                // Reading positions from table 3
317                chunk_index <<= 1u8;
318                loop {
319                    let table_3_proof_targets = subgroup_shuffle(
320                        table_3_proof_targets,
321                        SUBGROUP_SIZE / 2 * (chunk_index & 1) + subgroup_local_invocation_id / 2,
322                    );
323                    let table_3_proof_target = table_3_proof_targets[left_right];
324
325                    let table_2_proof_targets = if table_3_proof_target == Position::SENTINEL {
326                        [Position::SENTINEL; 2]
327                    } else {
328                        table_3_positions[table_3_proof_target as usize]
329                    };
330
331                    // Reading positions from table 2
332                    chunk_index <<= 1u8;
333                    loop {
334                        let table_2_proof_targets = subgroup_shuffle(
335                            table_2_proof_targets,
336                            SUBGROUP_SIZE / 2 * (chunk_index & 1)
337                                + subgroup_local_invocation_id / 2,
338                        );
339                        let table_2_proof_target = table_2_proof_targets[left_right];
340
341                        let [x_left, x_right] = if table_2_proof_target == Position::SENTINEL {
342                            [Position::SENTINEL; 2]
343                        } else {
344                            table_2_positions[table_2_proof_target as usize]
345                        };
346
347                        let global_x_left_offset =
348                            subgroup_local_invocation_id * 2 + chunk_index * SUBGROUP_SIZE * 2;
349                        let group_proof_index = global_x_left_offset / PROOF_X_SOURCES as u32;
350                        let x_left_offset = global_x_left_offset % PROOF_X_SOURCES as u32;
351                        let global_proof_index =
352                            positions_group_index * SUBGROUP_SIZE + group_proof_index;
353
354                        let proof_base = global_proof_index as usize * PROOF_U32_WORDS;
355                        let first_proof_word_index =
356                            ((u32::from(K) * x_left_offset) / u32::BITS) as usize;
357
358                        // TODO: Writes below can be optimized by building the full proof into the
359                        //  registers first and only write final result without atomics to global
360                        //  memory
361
362                        let mut local_proof_words = [0u32; 3];
363                        let x_left_offset_in_bits = u32::from(K) * x_left_offset;
364                        {
365                            let bit_offset = x_left_offset_in_bits % u32::BITS;
366                            let x_shifted_to_start = x_left << (u32::BITS - u32::from(K));
367
368                            let first_word = x_shifted_to_start >> bit_offset;
369                            let second_word =
370                                x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
371
372                            local_proof_words[0] = first_word;
373                            local_proof_words[1] = second_word;
374                        }
375
376                        let max_local_proof_word_index = {
377                            let x_right_offset_in_bits = x_left_offset_in_bits + u32::from(K);
378                            let local_proof_words_index = (x_right_offset_in_bits / u32::BITS)
379                                as usize
380                                - first_proof_word_index;
381                            let bit_offset = x_right_offset_in_bits % u32::BITS;
382                            let x_shifted_to_start = x_right << (u32::BITS - u32::from(K));
383
384                            let first_word = x_shifted_to_start >> bit_offset;
385                            let second_word =
386                                x_shifted_to_start.unbounded_shl(u32::BITS - bit_offset);
387
388                            local_proof_words[local_proof_words_index] |= first_word;
389                            local_proof_words[local_proof_words_index + 1] = second_word;
390
391                            local_proof_words_index + usize::from(second_word != 0)
392                        };
393
394                        // The first word is written unconditionally
395                        {
396                            // SAFETY: The whole proof is initialized at the beginning of the
397                            // function
398                            let word = unsafe {
399                                proofs[proof_base + first_proof_word_index].assume_init_mut()
400                            };
401                            // SAFETY: TODO: Probably should not be unsafe to begin with:
402                            //  https://github.com/Rust-GPU/rust-gpu/pull/394#issuecomment-3316594485
403                            unsafe {
404                                atomic_or::<
405                                    _,
406                                    { Scope::Subgroup as u32 },
407                                    { Semantics::NONE.bits() },
408                                >(
409                                    word, local_proof_words[0].to_be()
410                                );
411                            }
412                        }
413                        // Process remaining words, the loop is unrolled to save vector registers
414                        if max_local_proof_word_index > 0 {
415                            // SAFETY: The whole proof is initialized at the beginning of the
416                            // function
417                            let word = unsafe {
418                                proofs[proof_base + first_proof_word_index + 1].assume_init_mut()
419                            };
420                            // SAFETY: TODO: Probably should not be unsafe to begin with:
421                            //  https://github.com/Rust-GPU/rust-gpu/pull/394#issuecomment-3316594485
422                            unsafe {
423                                atomic_or::<
424                                    _,
425                                    { Scope::Subgroup as u32 },
426                                    { Semantics::NONE.bits() },
427                                >(
428                                    word, local_proof_words[1].to_be()
429                                );
430                            }
431                        }
432                        if max_local_proof_word_index > 1 {
433                            // SAFETY: The whole proof is initialized at the beginning of the
434                            // function
435                            let word = unsafe {
436                                proofs[proof_base + first_proof_word_index + 2].assume_init_mut()
437                            };
438                            // SAFETY: TODO: Probably should not be unsafe to begin with:
439                            //  https://github.com/Rust-GPU/rust-gpu/pull/394#issuecomment-3316594485
440                            unsafe {
441                                atomic_or::<
442                                    _,
443                                    { Scope::Subgroup as u32 },
444                                    { Semantics::NONE.bits() },
445                                >(
446                                    word, local_proof_words[2].to_be()
447                                );
448                            }
449                        }
450
451                        if chunk_index & 1 == 1 {
452                            break;
453                        }
454                        chunk_index += 1;
455                    }
456                    chunk_index >>= 1u8;
457
458                    if chunk_index & 1 == 1 {
459                        break;
460                    }
461                    chunk_index += 1;
462                }
463                chunk_index >>= 1u8;
464
465                if chunk_index & 1 == 1 {
466                    break;
467                }
468                chunk_index += 1;
469            }
470            chunk_index >>= 1u8;
471
472            if chunk_index & 1 == 1 {
473                break;
474            }
475            chunk_index += 1;
476        }
477        chunk_index >>= 1u8;
478
479        if chunk_index & 1 == 1 {
480            break;
481        }
482        chunk_index += 1;
483    }
484}
485
486/// NOTE: bucket sizes are zeroed after use
487// TODO: Maybe split `found_proofs` and `proofs` searching into separate shaders, such that less
488//  compute is wasted on searching proofs overall (right now up to half of the compute is wasted
489//  when computing proofs). It'll also be easier to add hashing after proof computation that way.
490#[spirv(compute(threads(256), entry_point_name = "find_proofs"))]
491#[expect(
492    clippy::too_many_arguments,
493    reason = "Both I/O and Vulkan stuff together take a lot of arguments"
494)]
495pub fn find_proofs(
496    #[spirv(workgroup_id)] workgroup_id: UVec3,
497    #[spirv(local_invocation_id)] local_invocation_id: UVec3,
498    #[spirv(subgroup_id)] subgroup_id: u32,
499    #[spirv(subgroup_size)] subgroup_size: u32,
500    #[spirv(num_subgroups)] num_subgroups: u32,
501    #[spirv(subgroup_local_invocation_id)] subgroup_local_invocation_id: u32,
502    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)]
503    table_2_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
504    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)]
505    table_3_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
506    #[spirv(storage_buffer, descriptor_set = 0, binding = 2)]
507    table_4_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
508    #[spirv(storage_buffer, descriptor_set = 0, binding = 3)]
509    table_5_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
510    #[spirv(storage_buffer, descriptor_set = 0, binding = 4)]
511    table_6_positions: &[[Position; 2]; REDUCED_MATCHES_COUNT * NUM_MATCH_BUCKETS],
512    #[spirv(storage_buffer, descriptor_set = 0, binding = 5)] bucket_sizes: &mut [u32;
513             NUM_S_BUCKETS],
514    #[spirv(storage_buffer, descriptor_set = 0, binding = 6)] buckets: &[[ProofTargets; NUM_ELEMENTS_PER_S_BUCKET];
515         NUM_S_BUCKETS],
516    #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] proofs: &mut Proofs,
517    #[spirv(workgroup)] found_proofs_scratch: &mut [MaybeUninit<u32>;
518             (WORKGROUP_SIZE / u32::BITS) as usize],
519) {
520    let local_invocation_id = local_invocation_id.x;
521    let workgroup_id = workgroup_id.x;
522
523    let global_subgroup_id = workgroup_id * num_subgroups + subgroup_id;
524
525    let positions_group_index = global_subgroup_id;
526    // Specify some common subgroup sizes so the driver can easily eliminate dead code. This is
527    // important because `local_words` inside the function is generic and impacts the number of
528    // registers used, so we want to minimize them.
529    match subgroup_size {
530        // Hypothetically possible
531        1 => {
532            find_proofs_impl::<1>(
533                local_invocation_id,
534                subgroup_id,
535                subgroup_local_invocation_id,
536                positions_group_index,
537                table_2_positions,
538                table_3_positions,
539                table_4_positions,
540                table_5_positions,
541                table_6_positions,
542                bucket_sizes,
543                buckets,
544                &mut proofs.found_proofs,
545                &mut proofs.proofs,
546                found_proofs_scratch,
547            );
548        }
549        // Hypothetically possible
550        2 => {
551            find_proofs_impl::<2>(
552                local_invocation_id,
553                subgroup_id,
554                subgroup_local_invocation_id,
555                positions_group_index,
556                table_2_positions,
557                table_3_positions,
558                table_4_positions,
559                table_5_positions,
560                table_6_positions,
561                bucket_sizes,
562                buckets,
563                &mut proofs.found_proofs,
564                &mut proofs.proofs,
565                found_proofs_scratch,
566            );
567        }
568        // LLVMpipe (Mesa 24, SSE)
569        4 => {
570            find_proofs_impl::<4>(
571                local_invocation_id,
572                subgroup_id,
573                subgroup_local_invocation_id,
574                positions_group_index,
575                table_2_positions,
576                table_3_positions,
577                table_4_positions,
578                table_5_positions,
579                table_6_positions,
580                bucket_sizes,
581                buckets,
582                &mut proofs.found_proofs,
583                &mut proofs.proofs,
584                found_proofs_scratch,
585            );
586        }
587        // LLVMpipe (Mesa 25, AVX/AVX2)
588        8 => {
589            find_proofs_impl::<8>(
590                local_invocation_id,
591                subgroup_id,
592                subgroup_local_invocation_id,
593                positions_group_index,
594                table_2_positions,
595                table_3_positions,
596                table_4_positions,
597                table_5_positions,
598                table_6_positions,
599                bucket_sizes,
600                buckets,
601                &mut proofs.found_proofs,
602                &mut proofs.proofs,
603                found_proofs_scratch,
604            );
605        }
606        // Raspberry PI 5
607        16 => {
608            find_proofs_impl::<16>(
609                local_invocation_id,
610                subgroup_id,
611                subgroup_local_invocation_id,
612                positions_group_index,
613                table_2_positions,
614                table_3_positions,
615                table_4_positions,
616                table_5_positions,
617                table_6_positions,
618                bucket_sizes,
619                buckets,
620                &mut proofs.found_proofs,
621                &mut proofs.proofs,
622                found_proofs_scratch,
623            );
624        }
625        // Intel/Nvidia
626        32 => {
627            find_proofs_impl::<32>(
628                local_invocation_id,
629                subgroup_id,
630                subgroup_local_invocation_id,
631                positions_group_index,
632                table_2_positions,
633                table_3_positions,
634                table_4_positions,
635                table_5_positions,
636                table_6_positions,
637                bucket_sizes,
638                buckets,
639                &mut proofs.found_proofs,
640                &mut proofs.proofs,
641                found_proofs_scratch,
642            );
643        }
644        // AMD
645        64 => {
646            find_proofs_impl::<64>(
647                local_invocation_id,
648                subgroup_id,
649                subgroup_local_invocation_id,
650                positions_group_index,
651                table_2_positions,
652                table_3_positions,
653                table_4_positions,
654                table_5_positions,
655                table_6_positions,
656                bucket_sizes,
657                buckets,
658                &mut proofs.found_proofs,
659                &mut proofs.proofs,
660                found_proofs_scratch,
661            );
662        }
663        // Hypothetically possible
664        128 => {
665            find_proofs_impl::<128>(
666                local_invocation_id,
667                subgroup_id,
668                subgroup_local_invocation_id,
669                positions_group_index,
670                table_2_positions,
671                table_3_positions,
672                table_4_positions,
673                table_5_positions,
674                table_6_positions,
675                bucket_sizes,
676                buckets,
677                &mut proofs.found_proofs,
678                &mut proofs.proofs,
679                found_proofs_scratch,
680            );
681        }
682        _ => {
683            // https://registry.khronos.org/vulkan/specs/latest/man/html/SubgroupSize.html
684            unreachable!("All Vulkan targets use power of two and subgroup size <= 128")
685        }
686    }
687}