Skip to main content

vyre_driver/
subgroup.rs

1//! Backend-neutral subgroup operation taxonomy.
2
3/// Canonical subgroup intrinsic operation.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5#[non_exhaustive]
6pub enum SubgroupOp {
7    /// Broadcast a value from one subgroup lane to all lanes.
8    Broadcast,
9    /// Reduce add across the subgroup.
10    Add,
11    /// Reduce max across the subgroup.
12    Max,
13    /// Reduce min across the subgroup.
14    Min,
15    /// Inclusive scan add across the subgroup.
16    InclusiveAdd,
17    /// Exclusive scan add across the subgroup.
18    ExclusiveAdd,
19    /// Shuffle-xor butterfly swap.
20    ShuffleXor,
21}
22
23impl SubgroupOp {
24    /// Iterate every canonical operation.
25    #[must_use]
26    pub const fn all() -> &'static [SubgroupOp] {
27        &[
28            SubgroupOp::Broadcast,
29            SubgroupOp::Add,
30            SubgroupOp::Max,
31            SubgroupOp::Min,
32            SubgroupOp::InclusiveAdd,
33            SubgroupOp::ExclusiveAdd,
34            SubgroupOp::ShuffleXor,
35        ]
36    }
37}
38
39/// Subgroup capability record shared by validation and optimizers.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct SubgroupCaps {
42    /// Native subgroup operations are available for compute.
43    pub supports_subgroup: bool,
44    /// Subgroup operations are available in vertex-stage contexts.
45    pub supports_subgroup_vertex: bool,
46    /// Subgroup size in lanes; `0` means unknown.
47    pub subgroup_size: u32,
48}
49
50impl SubgroupCaps {
51    /// Capability record for native subgroup intrinsics.
52    #[must_use]
53    pub const fn native(subgroup_size: u32) -> Self {
54        Self {
55            supports_subgroup: true,
56            supports_subgroup_vertex: false,
57            subgroup_size,
58        }
59    }
60
61    /// Capability record from a feature bit and reported lane-size range.
62    #[must_use]
63    pub const fn from_feature_range(
64        supports_feature: bool,
65        supports_vertex_stage: bool,
66        min_size: u32,
67        max_size: u32,
68    ) -> Self {
69        let supports_subgroup = supports_feature && min_size > 0 && max_size >= min_size;
70        Self {
71            supports_subgroup,
72            supports_subgroup_vertex: supports_vertex_stage && supports_subgroup,
73            subgroup_size: if supports_subgroup { min_size } else { 0 },
74        }
75    }
76
77    /// Return true when native subgroup operations are usable.
78    #[must_use]
79    pub const fn is_usable(self) -> bool {
80        self.supports_subgroup && self.subgroup_size > 0
81    }
82}
83
84/// Canonical lane offsets for a power-of-two full-subgroup tree reduction.
85#[must_use]
86pub fn reduction_offsets(subgroup_size: u32) -> Vec<u32> {
87    let mut offsets = Vec::new();
88    reduction_offsets_into(subgroup_size, &mut offsets);
89    offsets
90}
91
92/// Fallible canonical lane offsets for a full-subgroup tree reduction.
93///
94/// # Errors
95///
96/// Returns an error when the requested subgroup width cannot be rounded to a
97/// power-of-two reduction width or the output vector cannot reserve storage.
98pub fn try_reduction_offsets(subgroup_size: u32) -> Result<Vec<u32>, String> {
99    let mut offsets = Vec::new();
100    try_reduction_offsets_into(subgroup_size, &mut offsets)?;
101    Ok(offsets)
102}
103
104/// Write canonical reduction offsets into caller-owned storage.
105pub fn reduction_offsets_into(subgroup_size: u32, offsets: &mut Vec<u32>) {
106    // Clearing to empty on failure silently produces a degenerate reduction
107    // (no offsets -> the subgroup reduction reads nothing) — a silent fallback
108    // (Law 10). Fail loud; callers use try_reduction_offsets_into.
109    if let Err(error) = try_reduction_offsets_into(subgroup_size, offsets) {
110        panic!("vyre-driver reduction offsets generation failed: {error}");
111    }
112}
113
114/// Fallibly write canonical reduction offsets into caller-owned storage.
115///
116/// # Errors
117///
118/// Returns an error when the subgroup width overflows power-of-two rounding or
119/// the output vector cannot reserve the required offsets.
120pub fn try_reduction_offsets_into(
121    subgroup_size: u32,
122    offsets: &mut Vec<u32>,
123) -> Result<(), String> {
124    offsets.clear();
125    let Some(rounded_width) = subgroup_size.checked_next_power_of_two() else {
126        return Err(format!(
127            "subgroup reduction width {subgroup_size} cannot be rounded to a power of two. Fix: clamp subgroup size to a valid backend-reported hardware width."
128        ));
129    };
130    let offset_count = if subgroup_size == 0 {
131        0
132    } else {
133        rounded_width.ilog2() as usize
134    };
135    crate::allocation::try_reserve_vec_to_capacity(offsets, offset_count).map_err(|error| {
136        format!(
137            "subgroup reduction offsets could not reserve {offset_count} slot(s): {error}. Fix: reuse caller-owned offset storage or clamp subgroup size."
138        )
139    })?;
140    let mut width = rounded_width / 2;
141    while width > 0 {
142        offsets.push(width);
143        width /= 2;
144    }
145    Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn all_enumerates_seven_ops() {
154        assert_eq!(SubgroupOp::all().len(), 7);
155    }
156
157    #[test]
158    fn try_reduction_offsets_reuses_storage() {
159        let mut offsets = Vec::with_capacity(8);
160        let ptr = offsets.as_ptr();
161
162        try_reduction_offsets_into(32, &mut offsets).unwrap();
163
164        assert_eq!(offsets, [16, 8, 4, 2, 1]);
165        assert_eq!(offsets.as_ptr(), ptr);
166    }
167
168    #[test]
169    fn try_reduction_offsets_rejects_overflowing_rounding() {
170        let error = try_reduction_offsets(u32::MAX).unwrap_err();
171        assert!(error.contains("cannot be rounded to a power of two"));
172    }
173
174    #[test]
175    #[should_panic(expected = "reduction offsets generation failed")]
176    fn reduction_offsets_into_fails_loud_on_invalid_width() {
177        // An un-representable subgroup width must fail LOUD, not silently clear
178        // the buffer (which would yield a degenerate reduction; Law 10).
179        let mut offsets = vec![16, 8, 4];
180        reduction_offsets_into(u32::MAX, &mut offsets);
181    }
182
183    #[test]
184    #[should_panic(expected = "reduction offsets generation failed")]
185    fn reduction_offsets_owned_fails_loud_on_invalid_width() {
186        let _ = reduction_offsets(u32::MAX);
187    }
188}