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