Skip to main content

sim_lib_compute_wgpu/
pipeline.rs

1//! Bounded validated pipeline cache for portable wgpu tensor kernels.
2
3use std::collections::VecDeque;
4
5use sim_kernel::Symbol;
6
7use crate::{
8    WgpuAdapterProbe,
9    kernels::{PORTABLE_ELEMENTWISE_WGSL, PORTABLE_LINALG_WGSL, PORTABLE_REDUCTION_WGSL},
10};
11
12/// Portable operation implemented by a wgpu tensor kernel.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum WgpuKernelOp {
15    /// Element-wise addition.
16    Add,
17    /// Element-wise subtraction.
18    Sub,
19    /// Element-wise multiplication.
20    Mul,
21    /// Element-wise division.
22    Div,
23    /// Element-wise square root.
24    Sqrt,
25    /// Element-wise exponential.
26    Exp,
27    /// Element-wise sine.
28    Sin,
29    /// Element-wise cosine.
30    Cos,
31    /// Whole-tensor sum reduction.
32    Sum,
33    /// Whole-tensor minimum reduction.
34    Min,
35    /// Whole-tensor maximum reduction.
36    Max,
37    /// Whole-tensor Euclidean norm.
38    Norm,
39    /// Matrix transpose.
40    Transpose,
41    /// Vector dot product.
42    Dot,
43    /// Vector or matrix multiplication.
44    Matmul,
45}
46
47impl WgpuKernelOp {
48    /// Returns true when the kernel consumes two input tensors.
49    pub fn is_binary(self) -> bool {
50        matches!(self, Self::Add | Self::Sub | Self::Mul | Self::Div)
51    }
52
53    /// Returns true when the kernel consumes exactly one input tensor.
54    pub fn is_unary(self) -> bool {
55        matches!(
56            self,
57            Self::Sqrt
58                | Self::Exp
59                | Self::Sin
60                | Self::Cos
61                | Self::Sum
62                | Self::Min
63                | Self::Max
64                | Self::Norm
65                | Self::Transpose
66        )
67    }
68
69    /// Returns true when the kernel performs fixed-tree accumulation.
70    pub fn is_reduction(self) -> bool {
71        matches!(self, Self::Sum | Self::Min | Self::Max | Self::Norm)
72    }
73
74    /// Returns true when the kernel performs a linalg memory/product primitive.
75    pub fn is_linalg(self) -> bool {
76        matches!(self, Self::Transpose | Self::Dot | Self::Matmul)
77    }
78
79    fn symbol_name(self) -> &'static str {
80        match self {
81            Self::Add => "add",
82            Self::Sub => "sub",
83            Self::Mul => "mul",
84            Self::Div => "div",
85            Self::Sqrt => "sqrt",
86            Self::Exp => "exp",
87            Self::Sin => "sin",
88            Self::Cos => "cos",
89            Self::Sum => "sum",
90            Self::Min => "min",
91            Self::Max => "max",
92            Self::Norm => "norm",
93            Self::Transpose => "transpose",
94            Self::Dot => "dot",
95            Self::Matmul => "matmul",
96        }
97    }
98
99    fn wgsl_bytes(self) -> usize {
100        if self.is_reduction() {
101            PORTABLE_REDUCTION_WGSL.len()
102        } else if self.is_linalg() {
103            PORTABLE_LINALG_WGSL.len()
104        } else {
105            PORTABLE_ELEMENTWISE_WGSL.len()
106        }
107    }
108}
109
110/// Dtype strategy selected for a portable kernel.
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum WgpuKernelDType {
113    /// Native f32 arithmetic.
114    F32,
115    /// Native f16 arithmetic, only when the adapter granted shader f16.
116    F16Native,
117    /// bf16/unsupported half inputs widened to f32 arithmetic.
118    Bf16WidenedToF32,
119}
120
121impl WgpuKernelDType {
122    fn symbol_name(self) -> &'static str {
123        match self {
124            Self::F32 => "f32",
125            Self::F16Native => "f16",
126            Self::Bf16WidenedToF32 => "f32-widened-half",
127        }
128    }
129}
130
131/// Cache key for a validated portable pipeline.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct WgpuPipelineKey {
134    /// Adapter ordinal from discovery evidence.
135    pub adapter_ordinal: usize,
136    /// Portable kernel operation.
137    pub op: WgpuKernelOp,
138    /// Kernel dtype strategy.
139    pub dtype: WgpuKernelDType,
140    /// Output rank.
141    pub rank: usize,
142    /// Bounded planner profile selected from granted limits.
143    pub tile: WgpuTileProfile,
144}
145
146/// Portable tile profile selected from granted adapter limits.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct WgpuTileProfile {
149    /// Workgroup width for one-dimensional kernels.
150    pub workgroup_width: u32,
151    /// Square matrix tile edge used by transpose and matmul.
152    pub matrix_tile: u32,
153    /// Number of workgroup-local reduction lanes.
154    pub reduction_lanes: u32,
155    /// Maximum resident bytes accepted by one planned dispatch.
156    pub max_dispatch_bytes: u64,
157}
158
159impl WgpuTileProfile {
160    /// Selects conservative portable tiles from granted adapter limits.
161    pub fn from_probe(probe: &WgpuAdapterProbe) -> Self {
162        let limits = &probe.adapter.granted_limits;
163        let max_x = limits.max_compute_workgroup_size_x.max(1);
164        let max_invocations = limits.max_compute_invocations_per_workgroup.max(1);
165        let workgroup_width = max_x.min(max_invocations).clamp(1, 256);
166        let matrix_tile = 16_u32.min(workgroup_width).min(max_invocations).max(1);
167        let reduction_lanes = workgroup_width.clamp(1, 256);
168        Self {
169            workgroup_width,
170            matrix_tile,
171            reduction_lanes,
172            max_dispatch_bytes: limits.max_buffer_size.max(4),
173        }
174    }
175}
176
177/// Validated pipeline evidence retained in the cache.
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub struct WgpuPipelineRecord {
180    /// Cache key.
181    pub key: WgpuPipelineKey,
182    /// Stable symbol for this validated pipeline.
183    pub symbol: Symbol,
184    /// WGSL source byte length.
185    pub wgsl_bytes: usize,
186}
187
188/// Snapshot of cache pressure and reuse.
189#[derive(Clone, Debug, Default, PartialEq, Eq)]
190pub struct WgpuPipelineCacheSnapshot {
191    /// Cached pipeline count.
192    pub entries: usize,
193    /// Number of cache hits.
194    pub hits: usize,
195    /// Number of cache misses.
196    pub misses: usize,
197    /// Number of oldest-entry evictions.
198    pub evictions: usize,
199}
200
201/// Small FIFO cache for validated pipelines.
202#[derive(Clone, Debug)]
203pub struct WgpuPipelineCache {
204    capacity: usize,
205    entries: VecDeque<WgpuPipelineRecord>,
206    hits: usize,
207    misses: usize,
208    evictions: usize,
209}
210
211impl WgpuPipelineCache {
212    /// Builds a cache with a bounded entry count.
213    pub fn new(capacity: usize) -> Self {
214        Self {
215            capacity: capacity.max(1),
216            entries: VecDeque::new(),
217            hits: 0,
218            misses: 0,
219            evictions: 0,
220        }
221    }
222
223    /// Returns an existing pipeline or validates and inserts one.
224    pub fn get_or_insert(
225        &mut self,
226        probe: &WgpuAdapterProbe,
227        op: WgpuKernelOp,
228        dtype: WgpuKernelDType,
229        rank: usize,
230    ) -> WgpuPipelineRecord {
231        let tile = WgpuTileProfile::from_probe(probe);
232        let key = WgpuPipelineKey {
233            adapter_ordinal: probe.adapter.ordinal,
234            op,
235            dtype,
236            rank,
237            tile,
238        };
239        if let Some(record) = self.entries.iter().find(|record| record.key == key) {
240            self.hits += 1;
241            return record.clone();
242        }
243        self.misses += 1;
244        if self.entries.len() == self.capacity {
245            self.entries.pop_front();
246            self.evictions += 1;
247        }
248        let record = WgpuPipelineRecord {
249            symbol: Symbol::qualified(
250                "compute.pipeline.wgpu",
251                format!(
252                    "{}/{}/{}/rank-{}",
253                    probe.adapter.ordinal,
254                    op.symbol_name(),
255                    dtype.symbol_name(),
256                    rank
257                ),
258            ),
259            key,
260            wgsl_bytes: op.wgsl_bytes(),
261        };
262        self.entries.push_back(record.clone());
263        record
264    }
265
266    /// Returns cache pressure and reuse counters.
267    pub fn snapshot(&self) -> WgpuPipelineCacheSnapshot {
268        WgpuPipelineCacheSnapshot {
269            entries: self.entries.len(),
270            hits: self.hits,
271            misses: self.misses,
272            evictions: self.evictions,
273        }
274    }
275}
276
277impl Default for WgpuPipelineCache {
278    fn default() -> Self {
279        Self::new(16)
280    }
281}