Skip to main content

inferencelayer/
whisper_gpu.rs

1//! Whisper's encoder on **wgpu** (Metal / Vulkan / DX12 / WebGPU). Whisper is a VANILLA
2//! bidirectional transformer, so this is simpler than the conformer: the FFN/projection GEMMs +
3//! layernorms reuse the encoder's kernels, and the only per-block custom kernel is plain scaled-dot
4//! attention (no rel-pos term — see [`crate::parakeet_gpu`] for the harder conformer variant). The
5//! conv stem stays on the CPU (runs once); the N transformer blocks run GPU-resident in one submit.
6//! The CPU [`WhisperEncoder`] is the parity oracle.
7
8use anyhow::Result;
9
10use crate::GpuCtx;
11use crate::encoder::{
12    GEMM2_TILES, GEMM3_TILES, act_code, enc_gemm2_src, enc_gemm3_src, gemm2_tier, gemm2_tile,
13    gemm3_tier, gemm3_tile,
14};
15use crate::encoder_weights::Act;
16use crate::forward::{make_bg, pipeline, uni};
17use crate::whisper::{Whisper, WhisperEncoder};
18
19/// Vanilla bidirectional self-attention, flash-style online softmax, one workgroup per (row, head).
20/// `score[i,j] = q[i]·k[j] · hd^-0.5` over all j, softmax, `out[i] = Σ p·v[j]`. Keys are processed in
21/// tiles of 128: the whole workgroup computes 128 key-dots in parallel (thread↔key), reduces once for
22/// the tile max and once for the tile denom, then accumulates the output with thread↔head-dim. This
23/// pays ~2 reductions per 128 keys instead of one full-workgroup reduction *per key* — the difference
24/// between barrier-bound and compute-bound at t=1500. No rel-pos term (cf. [`crate::parakeet_gpu`]).
25pub(crate) const VANILLA_ATTN: &str = r#"
26struct Meta { t: u32, heads: u32, hd: u32, p0: u32 }
27@group(0) @binding(0) var<storage, read> q: array<f32>;
28@group(0) @binding(1) var<storage, read> k: array<f32>;
29@group(0) @binding(2) var<storage, read> v: array<f32>;
30@group(0) @binding(3) var<storage, read_write> outp: array<f32>;
31@group(0) @binding(4) var<uniform> mt: Meta;
32const TILE: u32 = 128u;
33var<workgroup> q_sh: array<f32, 128>;   // this row's q (hd ≤ 128)
34var<workgroup> sc: array<f32, 128>;     // tile scores, then tile probs
35var<workgroup> red: array<f32, 128>;    // reduction scratch (max, then sum)
36@compute @workgroup_size(128)
37fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) tid: u32) {
38    let t = mt.t;
39    let hd = mt.hd;
40    let d = mt.heads * hd;
41    let i = wg.x;
42    let head = wg.y;
43    if (i >= t || head >= mt.heads) { return; }
44    let scale = 1.0 / sqrt(f32(hd));
45    let qbase = i * d + head * hd;
46    if (tid < hd) { q_sh[tid] = q[qbase + tid]; }
47    workgroupBarrier();
48
49    var acc = 0.0;          // output for dim `tid` (tid < hd), across all tiles
50    var run_max = -1e30;
51    var run_den = 0.0;
52    var tile0 = 0u;
53    loop {
54        if (tile0 >= t) { break; }
55        let j = tile0 + tid;
56        // 1) score for this thread's key (thread ↔ key), dot over hd with no barrier
57        var s = -1e30;
58        if (j < t) {
59            let kbase = j * d + head * hd;
60            var dot = 0.0;
61            for (var c = 0u; c < hd; c = c + 1u) { dot = dot + q_sh[c] * k[kbase + c]; }
62            s = dot * scale;
63        }
64        sc[tid] = s;
65        red[tid] = s;
66        workgroupBarrier();
67        // 2) tile max
68        for (var st = 64u; st > 0u; st = st >> 1u) {
69            if (tid < st) { red[tid] = max(red[tid], red[tid + st]); }
70            workgroupBarrier();
71        }
72        let nm = max(run_max, red[0]);
73        let corr = exp(run_max - nm);
74        workgroupBarrier();
75        // 3) probs relative to the new running max
76        var p = 0.0;
77        if (j < t) { p = exp(sc[tid] - nm); }
78        sc[tid] = p;
79        red[tid] = p;
80        workgroupBarrier();
81        // 4) tile denom
82        for (var st = 64u; st > 0u; st = st >> 1u) {
83            if (tid < st) { red[tid] = red[tid] + red[tid + st]; }
84            workgroupBarrier();
85        }
86        run_den = run_den * corr + red[0];
87        // 5) accumulate output (thread ↔ head-dim); v read once per key
88        if (tid < hd) {
89            var a = acc * corr;
90            let n = min(TILE, t - tile0);
91            for (var r = 0u; r < n; r = r + 1u) {
92                a = a + sc[r] * v[(tile0 + r) * d + head * hd + tid];
93            }
94            acc = a;
95        }
96        run_max = nm;
97        workgroupBarrier();     // sc/red consumed before the next tile overwrites them
98        tile0 = tile0 + TILE;
99    }
100    if (tid < hd) { outp[qbase + tid] = acc / run_den; }
101}
102"#;
103
104pub(crate) const LN_SRC: &str = r#"
105struct Meta { h: u32, p0: u32, p1: u32, p2: u32 }
106@group(0) @binding(0) var<storage, read> x: array<f32>;
107@group(0) @binding(1) var<storage, read> w: array<f32>;
108@group(0) @binding(2) var<storage, read> b: array<f32>;
109@group(0) @binding(3) var<storage, read_write> outp: array<f32>;
110@group(0) @binding(4) var<uniform> mt: Meta;
111var<workgroup> sh: array<f32, 256>;
112@compute @workgroup_size(256)
113fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) t: u32) {
114    let base = wg.x * mt.h;
115    var sum = 0.0;
116    for (var i = t; i < mt.h; i = i + 256u) { sum = sum + x[base + i]; }
117    sh[t] = sum;
118    workgroupBarrier();
119    for (var s = 128u; s > 0u; s = s >> 1u) { if (t < s) { sh[t] = sh[t] + sh[t + s]; } workgroupBarrier(); }
120    let mean = sh[0] / f32(mt.h);
121    workgroupBarrier();
122    var sq = 0.0;
123    for (var i = t; i < mt.h; i = i + 256u) { let d = x[base + i] - mean; sq = sq + d * d; }
124    sh[t] = sq;
125    workgroupBarrier();
126    for (var s = 128u; s > 0u; s = s >> 1u) { if (t < s) { sh[t] = sh[t] + sh[t + s]; } workgroupBarrier(); }
127    let inv = 1.0 / sqrt(sh[0] / f32(mt.h) + 1e-5);
128    for (var i = t; i < mt.h; i = i + 256u) { outp[base + i] = (x[base + i] - mean) * inv * w[i] + b[i]; }
129}
130"#;
131
132pub(crate) const ADD_SRC: &str = r#"
133struct Meta { n: u32, p0: u32, p1: u32, p2: u32 }
134@group(0) @binding(0) var<storage, read_write> dst: array<f32>;
135@group(0) @binding(1) var<storage, read> src: array<f32>;
136@group(0) @binding(2) var<uniform> mt: Meta;
137@compute @workgroup_size(256)
138fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
139    if (gid.x < mt.n) { dst[gid.x] = dst[gid.x] + src[gid.x]; }
140}
141"#;
142
143pub(crate) struct GpuLinear {
144    pub(crate) w: wgpu::Buffer,
145    pub(crate) b: wgpu::Buffer,
146    pub(crate) n: u32,
147    pub(crate) k: u32,
148    pub(crate) v3: bool,
149}
150
151impl GpuLinear {
152    pub(crate) fn new_v3(ctx: &GpuCtx, w: &[f32], b: &[f32], n: usize, k: usize) -> Self {
153        let mut wt = vec![0f32; n * k];
154        for nn in 0..n {
155            for kk in 0..k {
156                wt[kk * n + nn] = w[nn * k + kk];
157            }
158        }
159        Self {
160            w: ctx.storage(&wt),
161            b: ctx.storage(b),
162            n: n as u32,
163            k: k as u32,
164            v3: true,
165        }
166    }
167}
168
169pub(crate) struct GpuNorm {
170    pub(crate) w: wgpu::Buffer,
171    pub(crate) b: wgpu::Buffer,
172}
173
174struct GpuBlock {
175    n_attn: GpuNorm,
176    q: GpuLinear,
177    k: GpuLinear,
178    v: GpuLinear,
179    out: GpuLinear,
180    n_ff: GpuNorm,
181    fc1: GpuLinear,
182    fc2: GpuLinear,
183}
184
185pub struct WhisperEncoderGpu {
186    cpu: WhisperEncoder,
187    gemm2: Vec<wgpu::ComputePipeline>,
188    gemm3: Vec<wgpu::ComputePipeline>,
189    ln: wgpu::ComputePipeline,
190    add: wgpu::ComputePipeline,
191    attn: wgpu::ComputePipeline,
192    /// Absent when the host model replaced it with Identity (ARK-ASR-3B does).
193    ln_post: Option<GpuNorm>,
194    blocks: Vec<GpuBlock>,
195    d: usize,
196    heads: usize,
197    hd: usize,
198    ff: usize,
199    max_t: usize,
200}
201
202impl WhisperEncoderGpu {
203    pub fn new(ctx: &GpuCtx, cpu: WhisperEncoder, max_t: usize) -> Result<Self> {
204        let d = cpu.d;
205        let heads = cpu.blocks[0].attn.heads;
206        let hd = cpu.blocks[0].attn.hd;
207        anyhow::ensure!(hd <= 128, "attention supports head_dim ≤ 128");
208        let ff = cpu.blocks[0].fc1.n;
209        let norm = |g: &crate::whisper::LayerNorm| GpuNorm {
210            w: ctx.storage(&g.w),
211            b: ctx.storage(&g.b),
212        };
213        let lin = |l: &crate::whisper::Linear| GpuLinear::new_v3(ctx, &l.w, &l.b, l.n, l.k);
214        let blocks = cpu
215            .blocks
216            .iter()
217            .map(|b| GpuBlock {
218                n_attn: norm(&b.norm_attn),
219                q: lin(&b.attn.q),
220                k: lin(&b.attn.k),
221                v: lin(&b.attn.v),
222                out: lin(&b.attn.out),
223                n_ff: norm(&b.norm_ff),
224                fc1: lin(&b.fc1),
225                fc2: lin(&b.fc2),
226            })
227            .collect();
228        // Optional: an ASR-LLM host replaces this norm with Identity and applies its own before
229        // its adapter, so the checkpoint simply has no such tensor.
230        let ln_post = cpu.ln_post.as_ref().map(norm);
231        Ok(Self {
232            gemm2: GEMM2_TILES
233                .iter()
234                .map(|&(bm, bn)| pipeline(ctx, "w_gemm2", &enc_gemm2_src(false, bm, bn)))
235                .collect(),
236            gemm3: GEMM3_TILES
237                .iter()
238                .map(|&(bm, bn, bk)| pipeline(ctx, "w_gemm3", &enc_gemm3_src(false, bm, bn, bk)))
239                .collect(),
240            ln: pipeline(ctx, "w_ln", LN_SRC),
241            add: pipeline(ctx, "w_add", ADD_SRC),
242            attn: pipeline(ctx, "w_attn", VANILLA_ATTN),
243            cpu,
244            ln_post,
245            blocks,
246            d,
247            heads,
248            hd,
249            ff,
250            max_t,
251        })
252    }
253
254    pub fn cpu(&self) -> &WhisperEncoder {
255        &self.cpu
256    }
257
258    /// Encoder features `[t, d]` (t=1500) — conv stem on CPU, the transformer blocks GPU-resident.
259    pub fn forward(&self, ctx: &GpuCtx, mel: &[f32]) -> Result<Vec<f32>> {
260        let (stem, t) = self.cpu.stem(mel);
261        anyhow::ensure!(t <= self.max_t, "t {t} exceeds max_t {}", self.max_t);
262        let (d, ff) = (self.d, self.ff);
263
264        let xb = ctx.storage(&stem);
265        let nb = ctx.empty(t * d);
266        let qb = ctx.empty(t * d);
267        let kb = ctx.empty(t * d);
268        let vb = ctx.empty(t * d);
269        let sb = ctx.empty(t * d);
270        let hb = ctx.empty(t * ff);
271
272        let mut passes: Vec<(&wgpu::ComputePipeline, wgpu::BindGroup, u32, u32)> = Vec::new();
273        let mut keep: Vec<wgpu::Buffer> = Vec::new();
274
275        macro_rules! gemm {
276            ($x:expr, $lw:expr, $y:expr, $act:expr) => {{
277                let lw: &GpuLinear = $lw;
278                let flags = 1u32 | (act_code($act) << 8);
279                let meta = uni(ctx, bytemuck::cast_slice(&[t as u32, lw.n, lw.k, flags]));
280                let (pl, bm, bn) = if lw.v3 {
281                    let tile = gemm3_tile(t, lw.n as usize);
282                    (&self.gemm3[gemm3_tier(tile)], tile.0, tile.1)
283                } else {
284                    let tile = gemm2_tile(t, lw.n as usize);
285                    (&self.gemm2[gemm2_tier(tile)], tile.0, tile.1)
286                };
287                let bg = make_bg(ctx, pl, &[$x, &lw.w, &lw.b, $y], &meta);
288                passes.push((
289                    pl,
290                    bg,
291                    lw.n.div_ceil(bn as u32),
292                    (t as u32).div_ceil(bm as u32),
293                ));
294                keep.push(meta);
295            }};
296        }
297        macro_rules! ln {
298            ($x:expr, $n:expr, $y:expr) => {{
299                let meta = uni(ctx, bytemuck::cast_slice(&[d as u32, 0u32, 0u32, 0u32]));
300                let bg = make_bg(ctx, &self.ln, &[$x, &$n.w, &$n.b, $y], &meta);
301                passes.push((&self.ln, bg, t as u32, 1));
302                keep.push(meta);
303            }};
304        }
305        macro_rules! add {
306            ($dst:expr, $src:expr) => {{
307                let meta = uni(
308                    ctx,
309                    bytemuck::cast_slice(&[(t * d) as u32, 0u32, 0u32, 0u32]),
310                );
311                let bg = make_bg(ctx, &self.add, &[$dst, $src], &meta);
312                passes.push((&self.add, bg, ((t * d) as u32).div_ceil(256), 1));
313                keep.push(meta);
314            }};
315        }
316
317        for b in &self.blocks {
318            // attn: x += out(attn(q,k,v)) over LN(x)
319            ln!(&xb, b.n_attn, &nb);
320            gemm!(&nb, &b.q, &qb, None);
321            gemm!(&nb, &b.k, &kb, None);
322            gemm!(&nb, &b.v, &vb, None);
323            {
324                let meta = uni(
325                    ctx,
326                    bytemuck::cast_slice(&[t as u32, self.heads as u32, self.hd as u32, 0u32]),
327                );
328                let bg = make_bg(ctx, &self.attn, &[&qb, &kb, &vb, &nb], &meta);
329                passes.push((&self.attn, bg, t as u32, self.heads as u32));
330                keep.push(meta);
331            }
332            gemm!(&nb, &b.out, &sb, None);
333            add!(&xb, &sb);
334            // ffn: x += fc2(gelu(fc1(LN(x))))
335            ln!(&xb, b.n_ff, &nb);
336            gemm!(&nb, &b.fc1, &hb, Some(Act::GeluErf));
337            gemm!(&hb, &b.fc2, &sb, None);
338            add!(&xb, &sb);
339        }
340        // Final LN → `nb`, which is then the output. With no such norm (Identity in the host
341        // model) the last block's output IS the encoder output, so `xb` is what gets read back.
342        let out_buf = match &self.ln_post {
343            Some(ln) => {
344                ln!(&xb, ln, &nb);
345                &nb
346            }
347            None => &xb,
348        };
349
350        let mut enc = ctx
351            .device
352            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
353                label: Some("whisper_enc"),
354            });
355        {
356            let mut cpass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
357                label: Some("whisper_enc"),
358                timestamp_writes: None,
359            });
360            for (pl, bg, gx, gy) in &passes {
361                cpass.set_pipeline(pl);
362                cpass.set_bind_group(0, bg, &[]);
363                cpass.dispatch_workgroups(*gx, *gy, 1);
364            }
365        }
366        ctx.queue.submit(Some(enc.finish()));
367        let out = ctx.read(out_buf, t * d)?;
368        drop(keep);
369        Ok(out)
370    }
371}
372
373// =====================================================================================================
374// Whisper DECODER on wgpu — autoregressive greedy decode, GPU-resident.
375//
376// The encoder above is a clean batch win (3.6x). The decoder is a different shape: strictly
377// sequential (token N+1 depends on N), tiny per-step GEMVs, and one unavoidable GPU->CPU sync per
378// token to feed the next input. So this is NOT a native speedup at whisper-base — it exists so the
379// WHOLE model can run GPU-resident (browser/WebGPU, and larger models where per-step work amortizes
380// the sync). The argmax runs ON the GPU, so only a 4-byte token id crosses back each step, not the
381// 51865-float logits. It stays TOKEN-IDENTICAL to the CPU decoder (same f32 GEMM family, same
382// suppress/argmax rules). On native, keep the CPU path as the default; this is the selectable one.
383// =====================================================================================================
384
385/// Embed a single token: `x[i] = embed_tokens[id·d + i] + embed_positions[p·d + i]`.
386const DEC_EMBED: &str = r#"
387struct Meta { id: u32, p: u32, d: u32, pad: u32 }
388@group(0) @binding(0) var<storage, read>       et: array<f32>;
389@group(0) @binding(1) var<storage, read>       ep: array<f32>;
390@group(0) @binding(2) var<storage, read_write> x:  array<f32>;
391@group(0) @binding(3) var<uniform>             mt: Meta;
392@compute @workgroup_size(256)
393fn main(@builtin(global_invocation_id) g: vec3<u32>) {
394    if (g.x >= mt.d) { return; }
395    x[g.x] = et[mt.id * mt.d + g.x] + ep[mt.p * mt.d + g.x];
396}
397"#;
398
399/// Append one token's `[d]` vector to a `[max_len, d]` KV cache at row `p`.
400const DEC_WRITE: &str = r#"
401struct Meta { p: u32, d: u32, p0: u32, p1: u32 }
402@group(0) @binding(0) var<storage, read>       src: array<f32>;
403@group(0) @binding(1) var<storage, read_write> dst: array<f32>;
404@group(0) @binding(2) var<uniform>             mt:  Meta;
405@compute @workgroup_size(256)
406fn main(@builtin(global_invocation_id) g: vec3<u32>) {
407    if (g.x >= mt.d) { return; }
408    dst[mt.p * mt.d + g.x] = src[g.x];
409}
410"#;
411
412/// Single-query attention: one query row (all heads) against `t` cached keys/values.
413/// `score[j] = q·k[j]·hd^-0.5`, softmax, `out = Σ p·v[j]`. One workgroup per head. Serves BOTH the
414/// causal self-attention (t = pos+1 cached keys) and the cross-attention (t = 1500 encoder frames).
415const DEC_QATTN: &str = r#"
416struct Meta { heads: u32, hd: u32, t: u32, pad: u32 }
417@group(0) @binding(0) var<storage, read>       q: array<f32>;   // [hid]
418@group(0) @binding(1) var<storage, read>       k: array<f32>;   // [t, hid]
419@group(0) @binding(2) var<storage, read>       v: array<f32>;   // [t, hid]
420@group(0) @binding(3) var<storage, read_write> o: array<f32>;   // [hid]
421@group(0) @binding(4) var<uniform>             mt: Meta;
422var<workgroup> q_sh: array<f32, 128>;
423var<workgroup> sc:   array<f32, 1600>;   // scores/probs (>= 1500 encoder frames)
424var<workgroup> red:  array<f32, 256>;
425@compute @workgroup_size(256)
426fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) t: u32) {
427    let head = wg.x;
428    let hd = mt.hd;
429    let hid = mt.heads * hd;
430    let tk = mt.t;
431    let scale = 1.0 / sqrt(f32(hd));
432    if (t < hd) { q_sh[t] = q[head * hd + t]; }
433    workgroupBarrier();
434    for (var j = t; j < tk; j += 256u) {
435        let kb = j * hid + head * hd;
436        var d = 0.0;
437        for (var c = 0u; c < hd; c += 1u) { d += q_sh[c] * k[kb + c]; }
438        sc[j] = d * scale;
439    }
440    workgroupBarrier();
441    var lm = -3.0e38;
442    for (var j = t; j < tk; j += 256u) { lm = max(lm, sc[j]); }
443    red[t] = lm;
444    workgroupBarrier();
445    for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] = max(red[t], red[t + s]); } workgroupBarrier(); }
446    let m = red[0];
447    workgroupBarrier();
448    var ls = 0.0;
449    for (var j = t; j < tk; j += 256u) { let e = exp(sc[j] - m); sc[j] = e; ls += e; }
450    red[t] = ls;
451    workgroupBarrier();
452    for (var s = 128u; s > 0u; s >>= 1u) { if (t < s) { red[t] += red[t + s]; } workgroupBarrier(); }
453    let sm = red[0];
454    workgroupBarrier();
455    if (t < hd) {
456        var acc = 0.0;
457        for (var j = 0u; j < tk; j += 1u) { acc += sc[j] * v[j * hid + head * hd + t]; }
458        o[head * hd + t] = acc / sm;
459    }
460}
461"#;
462
463/// Masked argmax over the vocab: `mask[j] != 0` excludes token `j` (suppress / begin-suppress /
464/// language-restrict). First (lowest-index) maximum wins, matching the CPU's `if l > best`. Writes the
465/// chosen id (as a u32 bit pattern) to `tid[0]`.
466const DEC_ARGMAX: &str = r#"
467struct Meta { vocab: u32, p0: u32, p1: u32, p2: u32 }
468@group(0) @binding(0) var<storage, read>       logits: array<f32>;
469@group(0) @binding(1) var<storage, read>       mask:   array<u32>;
470@group(0) @binding(2) var<storage, read_write> tid:    array<u32>;
471@group(0) @binding(3) var<uniform>             mt:     Meta;
472var<workgroup> vb: array<f32, 256>;
473var<workgroup> ib: array<u32, 256>;
474@compute @workgroup_size(256)
475fn main(@builtin(local_invocation_index) t: u32) {
476    var lv = -3.0e38;
477    var li = 0u;
478    for (var j = t; j < mt.vocab; j += 256u) {
479        if (mask[j] == 0u && logits[j] > lv) { lv = logits[j]; li = j; }
480    }
481    vb[t] = lv;
482    ib[t] = li;
483    workgroupBarrier();
484    for (var s = 128u; s > 0u; s >>= 1u) {
485        if (t < s) {
486            if (vb[t + s] > vb[t] || (vb[t + s] == vb[t] && ib[t + s] < ib[t])) {
487                vb[t] = vb[t + s];
488                ib[t] = ib[t + s];
489            }
490        }
491        workgroupBarrier();
492    }
493    if (t == 0u) { tid[0] = ib[0]; }
494}
495"#;
496
497struct DecGpuBlock {
498    n_self: GpuNorm,
499    sq: GpuLinear,
500    sk: GpuLinear,
501    sv: GpuLinear,
502    so: GpuLinear,
503    n_cross: GpuNorm,
504    cq: GpuLinear,
505    ck: GpuLinear,
506    cv: GpuLinear,
507    co: GpuLinear,
508    n_ff: GpuNorm,
509    fc1: GpuLinear,
510    fc2: GpuLinear,
511}
512
513/// The Whisper decoder, resident on the GPU. Consumes a loaded [`Whisper`] for its weights + params.
514pub struct WhisperDecoderGpu {
515    gemm3: Vec<wgpu::ComputePipeline>,
516    ln: wgpu::ComputePipeline,
517    add: wgpu::ComputePipeline,
518    embed_pl: wgpu::ComputePipeline,
519    write_pl: wgpu::ComputePipeline,
520    qattn_pl: wgpu::ComputePipeline,
521    argmax_pl: wgpu::ComputePipeline,
522    embed_tokens: wgpu::Buffer,
523    embed_pos: wgpu::Buffer,
524    blocks: Vec<DecGpuBlock>,
525    ln_post: GpuNorm,
526    lm_head: GpuLinear,
527    mask_gen: wgpu::Buffer,
528    mask_gen0: wgpu::Buffer,
529    mask_lang: wgpu::Buffer,
530    d: usize,
531    heads: usize,
532    hd: usize,
533    ff: usize,
534    vocab: usize,
535    max_target: usize,
536    prompt: Vec<usize>,
537    lang_on: bool,
538    transcribe_token: usize,
539    notimestamps_token: usize,
540    eos: usize,
541}
542
543impl WhisperDecoderGpu {
544    pub fn new(ctx: &GpuCtx, cpu: &Whisper) -> Result<Self> {
545        let d = cpu.d;
546        let heads = cpu.blocks[0].self_attn.heads;
547        let hd = cpu.blocks[0].self_attn.hd;
548        anyhow::ensure!(hd <= 128, "decoder head_dim {hd} > 128");
549        let ff = cpu.blocks[0].fc1.n;
550        let vocab = cpu.vocab;
551        let norm = |g: &crate::whisper::LayerNorm| GpuNorm {
552            w: ctx.storage(&g.w),
553            b: ctx.storage(&g.b),
554        };
555        let lin = |l: &crate::whisper::Linear| GpuLinear::new_v3(ctx, &l.w, &l.b, l.n, l.k);
556        let blocks = cpu
557            .blocks
558            .iter()
559            .map(|b| DecGpuBlock {
560                n_self: norm(&b.norm_self),
561                sq: lin(&b.self_attn.q),
562                sk: lin(&b.self_attn.k),
563                sv: lin(&b.self_attn.v),
564                so: lin(&b.self_attn.out),
565                n_cross: norm(&b.norm_cross),
566                cq: lin(&b.cross_attn.q),
567                ck: lin(&b.cross_attn.k),
568                cv: lin(&b.cross_attn.v),
569                co: lin(&b.cross_attn.out),
570                n_ff: norm(&b.norm_ff),
571                fc1: lin(&b.fc1),
572                fc2: lin(&b.fc2),
573            })
574            .collect();
575        let mask_of = |set: &[usize], base_ones: bool| -> wgpu::Buffer {
576            let mut m = vec![if base_ones { 1u32 } else { 0u32 }; vocab];
577            for &t in set {
578                if t < vocab {
579                    m[t] = if base_ones { 0 } else { 1 };
580                }
581            }
582            ctx.storage_bytes(bytemuck::cast_slice(&m))
583        };
584        let mask_gen = mask_of(&cpu.suppress, false);
585        let mut m0 = vec![0u32; vocab];
586        for &t in cpu.suppress.iter().chain(cpu.begin_suppress.iter()) {
587            if t < vocab {
588                m0[t] = 1;
589            }
590        }
591        let mask_gen0 = ctx.storage_bytes(bytemuck::cast_slice(&m0));
592        let mask_lang = mask_of(&cpu.lang_tokens, true); // 1 everywhere except the language tokens
593
594        Ok(Self {
595            gemm3: GEMM3_TILES
596                .iter()
597                .map(|&(bm, bn, bk)| pipeline(ctx, "wd_gemm3", &enc_gemm3_src(false, bm, bn, bk)))
598                .collect(),
599            ln: pipeline(ctx, "wd_ln", LN_SRC),
600            add: pipeline(ctx, "wd_add", ADD_SRC),
601            embed_pl: pipeline(ctx, "wd_embed", DEC_EMBED),
602            write_pl: pipeline(ctx, "wd_write", DEC_WRITE),
603            qattn_pl: pipeline(ctx, "wd_qattn", DEC_QATTN),
604            argmax_pl: pipeline(ctx, "wd_argmax", DEC_ARGMAX),
605            embed_tokens: ctx.storage(&cpu.embed_tokens),
606            embed_pos: ctx.storage(&cpu.embed_pos),
607            ln_post: norm(&cpu.ln_post),
608            lm_head: lin(&cpu.lm_head),
609            blocks,
610            mask_gen,
611            mask_gen0,
612            mask_lang,
613            d,
614            heads,
615            hd,
616            ff,
617            vocab,
618            max_target: cpu.max_target,
619            prompt: cpu.prompt.clone(),
620            lang_on: !cpu.lang_tokens.is_empty(),
621            transcribe_token: cpu.transcribe_token,
622            notimestamps_token: cpu.notimestamps_token,
623            eos: cpu.eos,
624        })
625    }
626
627    /// Greedy decode from encoder features `[1500, d]` → token ids (prompt + generated + eos).
628    /// Token-identical to [`Whisper::generate_from_enc`].
629    pub fn generate(&self, ctx: &GpuCtx, enc: &[f32], max_new: usize) -> Result<Vec<usize>> {
630        let d = self.d;
631        let (heads, hd, ff, vocab) = (self.heads, self.hd, self.ff, self.vocab);
632        let n_cross = enc.len() / d;
633
634        let b_enc = ctx.storage(enc);
635        // Cross-attention K/V — projected once from the (fixed) encoder output.
636        let ckv: Vec<(wgpu::Buffer, wgpu::Buffer)> = self
637            .blocks
638            .iter()
639            .map(|_| (ctx.empty(n_cross * d), ctx.empty(n_cross * d)))
640            .collect();
641        // per-block self-attention caches + shared per-step scratch
642        let kc: Vec<wgpu::Buffer> = self
643            .blocks
644            .iter()
645            .map(|_| ctx.empty(self.max_target * d))
646            .collect();
647        let vc: Vec<wgpu::Buffer> = self
648            .blocks
649            .iter()
650            .map(|_| ctx.empty(self.max_target * d))
651            .collect();
652        let x = ctx.empty(d);
653        let normed = ctx.empty(d);
654        let q = ctx.empty(d);
655        let k_new = ctx.empty(d);
656        let v_new = ctx.empty(d);
657        let sa = ctx.empty(d);
658        let ca = ctx.empty(d);
659        let ffb = ctx.empty(ff);
660        let logits = ctx.empty(vocab);
661        let tid = ctx.empty(1);
662
663        type Pass<'a> = (&'a wgpu::ComputePipeline, wgpu::BindGroup, u32, u32);
664        macro_rules! gemm {
665            ($passes:expr, $keep:expr, $x:expr, $l:expr, $y:expr, $m:expr, $act:expr) => {{
666                let l: &GpuLinear = $l;
667                let m: usize = $m;
668                let flags = 1u32 | (act_code($act) << 8);
669                let meta = uni(ctx, bytemuck::cast_slice(&[m as u32, l.n, l.k, flags]));
670                let tile = gemm3_tile(m, l.n as usize);
671                let pl = &self.gemm3[gemm3_tier(tile)];
672                let bg = make_bg(ctx, pl, &[$x, &l.w, &l.b, $y], &meta);
673                $passes.push((
674                    pl,
675                    bg,
676                    l.n.div_ceil(tile.1 as u32),
677                    (m as u32).div_ceil(tile.0 as u32),
678                ));
679                $keep.push(meta);
680            }};
681        }
682        macro_rules! ln {
683            ($passes:expr, $keep:expr, $x:expr, $nm:expr, $y:expr) => {{
684                let nm: &GpuNorm = $nm;
685                let meta = uni(ctx, bytemuck::cast_slice(&[d as u32, 0u32, 0u32, 0u32]));
686                let bg = make_bg(ctx, &self.ln, &[$x, &nm.w, &nm.b, $y], &meta);
687                $passes.push((&self.ln, bg, 1u32, 1u32));
688                $keep.push(meta);
689            }};
690        }
691        macro_rules! add {
692            ($passes:expr, $keep:expr, $dst:expr, $src:expr) => {{
693                let meta = uni(ctx, bytemuck::cast_slice(&[d as u32, 0u32, 0u32, 0u32]));
694                let bg = make_bg(ctx, &self.add, &[$dst, $src], &meta);
695                $passes.push((&self.add, bg, (d as u32).div_ceil(256), 1u32));
696                $keep.push(meta);
697            }};
698        }
699        macro_rules! write {
700            ($passes:expr, $keep:expr, $src:expr, $dst:expr, $pos:expr) => {{
701                let meta = uni(
702                    ctx,
703                    bytemuck::cast_slice(&[$pos as u32, d as u32, 0u32, 0u32]),
704                );
705                let bg = make_bg(ctx, &self.write_pl, &[$src, $dst], &meta);
706                $passes.push((&self.write_pl, bg, (d as u32).div_ceil(256), 1u32));
707                $keep.push(meta);
708            }};
709        }
710        macro_rules! qattn {
711            ($passes:expr, $keep:expr, $q:expr, $k:expr, $v:expr, $o:expr, $tk:expr) => {{
712                let meta = uni(
713                    ctx,
714                    bytemuck::cast_slice(&[heads as u32, hd as u32, $tk as u32, 0u32]),
715                );
716                let bg = make_bg(ctx, &self.qattn_pl, &[$q, $k, $v, $o], &meta);
717                $passes.push((&self.qattn_pl, bg, heads as u32, 1u32));
718                $keep.push(meta);
719            }};
720        }
721
722        // Cross K/V precompute (one submit).
723        {
724            let mut passes: Vec<Pass> = Vec::new();
725            let mut keep: Vec<wgpu::Buffer> = Vec::new();
726            for (b, (cbk, cbv)) in self.blocks.iter().zip(&ckv) {
727                gemm!(passes, keep, &b_enc, &b.ck, cbk, n_cross, None);
728                gemm!(passes, keep, &b_enc, &b.cv, cbv, n_cross, None);
729            }
730            record_submit(ctx, &passes);
731            drop(keep);
732        }
733
734        // One decode step: fill the caches at `pos` and, when `emit` is set, produce the next token id.
735        let step =
736            |token: usize, pos: usize, emit: Option<&wgpu::Buffer>| -> Result<Option<usize>> {
737                let mut passes: Vec<Pass> = Vec::new();
738                let mut keep: Vec<wgpu::Buffer> = Vec::new();
739                {
740                    let meta = uni(
741                        ctx,
742                        bytemuck::cast_slice(&[token as u32, pos as u32, d as u32, 0u32]),
743                    );
744                    let bg = make_bg(
745                        ctx,
746                        &self.embed_pl,
747                        &[&self.embed_tokens, &self.embed_pos, &x],
748                        &meta,
749                    );
750                    passes.push((&self.embed_pl, bg, (d as u32).div_ceil(256), 1u32));
751                    keep.push(meta);
752                }
753                for (bi, b) in self.blocks.iter().enumerate() {
754                    ln!(passes, keep, &x, &b.n_self, &normed);
755                    gemm!(passes, keep, &normed, &b.sq, &q, 1, None);
756                    gemm!(passes, keep, &normed, &b.sk, &k_new, 1, None);
757                    gemm!(passes, keep, &normed, &b.sv, &v_new, 1, None);
758                    write!(passes, keep, &k_new, &kc[bi], pos);
759                    write!(passes, keep, &v_new, &vc[bi], pos);
760                    qattn!(passes, keep, &q, &kc[bi], &vc[bi], &sa, pos + 1);
761                    gemm!(passes, keep, &sa, &b.so, &normed, 1, None);
762                    add!(passes, keep, &x, &normed);
763                    ln!(passes, keep, &x, &b.n_cross, &normed);
764                    gemm!(passes, keep, &normed, &b.cq, &q, 1, None);
765                    qattn!(passes, keep, &q, &ckv[bi].0, &ckv[bi].1, &ca, n_cross);
766                    gemm!(passes, keep, &ca, &b.co, &normed, 1, None);
767                    add!(passes, keep, &x, &normed);
768                    ln!(passes, keep, &x, &b.n_ff, &normed);
769                    gemm!(passes, keep, &normed, &b.fc1, &ffb, 1, Some(Act::GeluErf));
770                    gemm!(passes, keep, &ffb, &b.fc2, &normed, 1, None);
771                    add!(passes, keep, &x, &normed);
772                }
773                if let Some(mask) = emit {
774                    ln!(passes, keep, &x, &self.ln_post, &normed);
775                    gemm!(passes, keep, &normed, &self.lm_head, &logits, 1, None);
776                    let meta = uni(ctx, bytemuck::cast_slice(&[vocab as u32, 0u32, 0u32, 0u32]));
777                    let bg = make_bg(ctx, &self.argmax_pl, &[&logits, mask, &tid], &meta);
778                    passes.push((&self.argmax_pl, bg, 1u32, 1u32));
779                    keep.push(meta);
780                }
781                record_submit(ctx, &passes);
782                drop(keep);
783                if emit.is_some() {
784                    Ok(Some(ctx.read(&tid, 1)?[0].to_bits() as usize))
785                } else {
786                    Ok(None)
787                }
788            };
789
790        // Language detection (one step from SOT, restricted to the language tokens), then the prompt.
791        let sot = self.prompt[0];
792        let lang = if self.lang_on {
793            step(sot, 0, Some(&self.mask_lang))?.expect("emit")
794        } else {
795            self.prompt[1]
796        };
797        let prompt = vec![sot, lang, self.transcribe_token, self.notimestamps_token];
798        let mut ids = prompt.clone();
799        for (i, &t) in prompt.iter().enumerate().take(prompt.len() - 1) {
800            step(t, i, None)?; // prefill: fill the cache, no logits
801        }
802        let cap = (prompt.len() + max_new).min(self.max_target);
803        let mut pos = prompt.len() - 1;
804        let mut tok = prompt[pos];
805        let mut first = true;
806        loop {
807            let mask = if first {
808                &self.mask_gen0
809            } else {
810                &self.mask_gen
811            };
812            let nxt = step(tok, pos, Some(mask))?.expect("emit");
813            ids.push(nxt);
814            if nxt == self.eos || ids.len() >= cap {
815                break;
816            }
817            pos += 1;
818            tok = nxt;
819            first = false;
820        }
821        Ok(ids)
822    }
823}
824
825/// Record a linear list of dispatches into one compute pass and submit it.
826fn record_submit(ctx: &GpuCtx, passes: &[(&wgpu::ComputePipeline, wgpu::BindGroup, u32, u32)]) {
827    let mut enc = ctx
828        .device
829        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
830            label: Some("whisper_dec"),
831        });
832    {
833        let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
834        for (pl, bg, gx, gy) in passes {
835            p.set_pipeline(pl);
836            p.set_bind_group(0, bg, &[]);
837            p.dispatch_workgroups(*gx, *gy, 1);
838        }
839    }
840    ctx.queue.submit(Some(enc.finish()));
841}