Skip to main content

ab_proof_of_space_gpu/shader/
find_proofs.rs

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