Skip to main content

kime_cuda/
plan.rs

1//! Graphs lowered for the GPU: every value gets an element type and a place in one device arena,
2//! every GEMM its cuBLASLt setup, and a run is a copy of the batch's index tables, a fixed list of
3//! launches and a copy of the outputs back. Lowering captures all three as one CUDA graph, with both
4//! copies going through page locked host buffers, so a run is one graph launch and one wait.
5
6use std::sync::OnceLock;
7
8use cudarc::driver::{
9    CudaGraph, CudaSlice, DevicePtr, LaunchConfig, PinnedHostSlice, PushKernelArg, result, sys,
10};
11use kime_tensor::plan::{Epilogue, Graph, Op, Rows, Val, layout};
12use kime_tensor::{Backend, Batch, Bucket, Caps, Error, HostTensor, Outputs, Result};
13
14use crate::lt::{self, Ty};
15use crate::tune::{self, Key};
16use crate::{CudaBackend, Precision, WORKSPACE, dev};
17
18/// Width of one attention head.
19const HEAD: usize = 64;
20
21/// Query rows and warps per attention block, `ATT_Q` and `ATT_W` in the kernels.
22pub(crate) const ATT_Q: usize = 16;
23pub(crate) const ATT_W: u32 = 8;
24
25/// Rows per layer norm block, one warp each, `LN_ROWS` in the kernels.
26pub(crate) const LN_ROWS: usize = 4;
27
28/// A weight on the device in FP32, with an FP16 copy made the first time a plan needs one.
29struct Tensor {
30    shape: Vec<usize>,
31    f32: CudaSlice<f32>,
32    f16: OnceLock<CudaSlice<u16>>,
33}
34
35/// Every weight of a checkpoint on one GPU.
36pub struct Weights(Vec<Tensor>);
37
38impl std::fmt::Debug for Weights {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("Weights").field("tensors", &self.0.len()).finish_non_exhaustive()
41    }
42}
43
44/// A value resolved to its type and place.
45#[derive(Debug, Clone, Copy)]
46struct Loc {
47    ptr: u64,
48    rows: Rows,
49    width: usize,
50    ty: Ty,
51}
52
53impl Loc {
54    fn half(self) -> bool {
55        self.ty == Ty::F16
56    }
57}
58
59fn kind(r: Rows) -> i32 {
60    match r {
61        Rows::Tokens => 0,
62        Rows::Seqs => 1,
63        Rows::Markers => 2,
64    }
65}
66
67#[derive(Debug, Clone, Copy)]
68enum Step {
69    Embed {
70        table: u64,
71        out: Loc,
72    },
73    LayerNorm {
74        x: Loc,
75        w: u64,
76        b: u64,
77        eps: f32,
78        out: Loc,
79    },
80    /// `len` FP32 values to FP16 in the staging buffer, for a GEMM with an FP32 input and an FP16
81    /// output.
82    ToHalf {
83        x: u64,
84        len: u64,
85    },
86    /// A GEMM, then the bias and activation when there are any.
87    Gemm {
88        gemm: usize,
89        b: u64,
90        act: i32,
91        out: Loc,
92    },
93    Rope {
94        qkv: Loc,
95        cos: u64,
96        sin: u64,
97    },
98    /// Attention, with rope applied to q and k as they are loaded when the tables are not null.
99    Attention {
100        qkv: Loc,
101        cos: u64,
102        sin: u64,
103        window: i32,
104        out: Loc,
105    },
106    GeGlu {
107        x: Loc,
108        out: Loc,
109    },
110    AddType {
111        h: Loc,
112        table: u64,
113    },
114    Gather {
115        h: Loc,
116        out: Loc,
117    },
118    ActFeatures {
119        h: Loc,
120        logits: Loc,
121        out: Loc,
122    },
123}
124
125impl Step {
126    fn name(&self) -> &'static str {
127        match self {
128            Step::Embed { .. } => "embed",
129            Step::LayerNorm { .. } => "layer norm",
130            Step::ToHalf { .. } => "to half",
131            Step::Gemm { .. } => "gemm",
132            Step::Rope { .. } => "rope",
133            Step::Attention { .. } => "attention",
134            Step::GeGlu { .. } => "geglu",
135            Step::AddType { .. } => "type embedding",
136            Step::Gather { .. } => "gather markers",
137            Step::ActFeatures { .. } => "act features",
138        }
139    }
140}
141
142/// Where each index table sits in the staging buffer, in u32 elements.
143#[derive(Debug, Clone, Copy)]
144struct Index {
145    ids: usize,
146    pos: usize,
147    seq: usize,
148    cu: usize,
149    mcu: usize,
150    mrow: usize,
151    qtype: usize,
152    tile: usize,
153    len: usize,
154}
155
156impl Index {
157    fn new(b: Bucket) -> Self {
158        let ids = 4;
159        let pos = ids + b.tokens;
160        let seq = pos + b.tokens;
161        let cu = seq + b.tokens;
162        let mcu = cu + b.seqs + 1;
163        let mrow = mcu + b.seqs + 1;
164        let qtype = mrow + b.markers;
165        let tile = qtype + b.seqs;
166        Self { ids, pos, seq, cu, mcu, mrow, qtype, tile, len: tile + att_blocks(b) }
167    }
168}
169
170/// Device addresses of the batch's count buffer and index tables.
171#[derive(Debug, Clone, Copy)]
172struct Ptrs {
173    n: u64,
174    ids: u64,
175    pos: u64,
176    seq: u64,
177    cu: u64,
178    mcu: u64,
179    mrow: u64,
180    qtype: u64,
181    tile: u64,
182}
183
184impl Ptrs {
185    fn new(base: u64, at: Index) -> Self {
186        let a = |o: usize| base + 4 * o as u64;
187        Self {
188            n: base,
189            ids: a(at.ids),
190            pos: a(at.pos),
191            seq: a(at.seq),
192            cu: a(at.cu),
193            mcu: a(at.mcu),
194            mrow: a(at.mrow),
195            qtype: a(at.qtype),
196            tile: a(at.tile),
197        }
198    }
199}
200
201/// A graph lowered for one bucket on one GPU.
202pub struct CudaPlan {
203    bucket: Bucket,
204    steps: Vec<Step>,
205    gemms: Vec<lt::Gemm>,
206    arena: CudaSlice<u8>,
207    /// FP16 copies of FP32 GEMM inputs.
208    _stage: CudaSlice<u16>,
209    stage: u64,
210    /// Held so the rope tables the steps point at stay alive.
211    _ropes: Vec<(u64, CudaSlice<f32>, CudaSlice<f32>)>,
212    _index: CudaSlice<u32>,
213    /// The index tables on the host, page locked so the copy in the graph is asynchronous.
214    index_host: PinnedHostSlice<u32>,
215    at: Index,
216    ptrs: Ptrs,
217    logits: Loc,
218    act: Loc,
219    /// The bucket's logits then its act outputs, page locked.
220    host_out: PinnedHostSlice<f32>,
221    graph: Option<Captured>,
222    profile: Option<Vec<u64>>,
223    /// Runs timed since profiling started.
224    profiled: u64,
225}
226
227/// A captured run.
228struct Captured(CudaGraph);
229
230// SAFETY: a CUDA graph exec may be launched from any thread as long as calls on it are serialized.
231// The plan is only used through `&mut CudaPlan`, so they are.
232unsafe impl Send for Captured {}
233
234impl std::fmt::Debug for CudaPlan {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        f.debug_struct("CudaPlan")
237            .field("bucket", &self.bucket)
238            .field("steps", &self.steps.len())
239            .field("arena", &self.arena.len())
240            .finish_non_exhaustive()
241    }
242}
243
244impl CudaPlan {
245    /// The bucket it was built for.
246    #[must_use]
247    pub fn bucket(&self) -> Bucket {
248        self.bucket
249    }
250
251    /// Arena size in bytes.
252    #[must_use]
253    pub fn arena_bytes(&self) -> usize {
254        self.arena.len()
255    }
256
257    /// Times every step from now on. Each step then waits for the GPU and the graph is not used,
258    /// so this is for finding where the time goes, not for measuring the total.
259    pub fn profile(&mut self) {
260        self.profile = Some(vec![0; self.steps.len()]);
261        self.profiled = 0;
262    }
263
264    /// Time per GEMM shape since [`CudaPlan::profile`]: rows, inner size, columns, calls over all
265    /// timed runs and nanoseconds, largest first.
266    #[must_use]
267    pub fn gemm_timings(&self) -> Vec<((usize, usize, usize), u64, u64)> {
268        let mut by: Vec<((usize, usize, usize), u64, u64)> = Vec::new();
269        for (s, &ns) in self.steps.iter().zip(self.profile.iter().flatten()) {
270            if let Step::Gemm { gemm, .. } = *s {
271                let d = self.gemms[gemm].dims;
272                match by.iter_mut().find(|b| b.0 == d) {
273                    Some(b) => {
274                        b.1 += self.profiled;
275                        b.2 += ns;
276                    }
277                    None => by.push((d, self.profiled, ns)),
278                }
279            }
280        }
281        by.sort_by_key(|b| std::cmp::Reverse(b.2));
282        by
283    }
284
285    /// Time per kind of step since [`CudaPlan::profile`], in nanoseconds, largest first.
286    #[must_use]
287    pub fn timings(&self) -> Vec<(&'static str, u64)> {
288        let mut by: Vec<(&'static str, u64)> = Vec::new();
289        for (s, &ns) in self.steps.iter().zip(self.profile.iter().flatten()) {
290            match by.iter_mut().find(|b| b.0 == s.name()) {
291                Some(b) => b.1 += ns,
292                None => by.push((s.name(), ns)),
293            }
294        }
295        by.sort_by_key(|b| std::cmp::Reverse(b.1));
296        by
297    }
298}
299
300/// The element type of every value.
301///
302/// With [`Precision::F32`] that is FP32 throughout. With [`Precision::F16`] it is FP16 unless the
303/// value is the residual stream (written by the embedding or accumulated into), the type
304/// embedding's target, the act features, an output, attention's qkv, or computed from an FP32 value
305/// by a gather or by a GEMM whose output is not an attention or GeGLU output. The qkv stays FP32
306/// because ModernBERT's attention scores are large enough that FP16 rounding of q and k moves the
307/// softmax by more than the probability bound. A GEMM with an FP32 input and an FP16 output
308/// converts its input first.
309fn types(graph: &Graph, precision: Precision) -> Vec<Ty> {
310    let n = graph.vals.len();
311    if precision == Precision::F32 {
312        return vec![Ty::F32; n];
313    }
314    let mut ty = vec![Ty::F16; n];
315    let mut half = vec![false; n];
316    for op in &graph.ops {
317        match *op {
318            Op::Embed { out, .. } | Op::ActFeatures { out, .. } => ty[out.0 as usize] = Ty::F32,
319            Op::Gemm { epilogue: Epilogue::Accumulate, out, .. } => ty[out.0 as usize] = Ty::F32,
320            Op::AddType { h, .. } => ty[h.0 as usize] = Ty::F32,
321            Op::Attention { qkv, out, .. } => {
322                ty[qkv.0 as usize] = Ty::F32;
323                half[out.0 as usize] = true;
324            }
325            Op::GeGlu { out, .. } => half[out.0 as usize] = true,
326            _ => {}
327        }
328    }
329    for v in [graph.logits, graph.act].into_iter().flatten() {
330        ty[v.0 as usize] = Ty::F32;
331    }
332    loop {
333        let mut changed = false;
334        for op in &graph.ops {
335            let (a, out) = match *op {
336                Op::Gemm { a, out, .. } => (a, out),
337                Op::GatherMarkers { h, out } => (h, out),
338                _ => continue,
339            };
340            let o = out.0 as usize;
341            if ty[a.0 as usize] == Ty::F32 && ty[o] == Ty::F16 && !half[o] {
342                ty[o] = Ty::F32;
343                changed = true;
344            }
345        }
346        if !changed {
347            return ty;
348        }
349    }
350}
351
352/// Grid for one block per row.
353fn rows(n: usize, threads: u32) -> LaunchConfig {
354    LaunchConfig { grid_dim: (n as u32, 1, 1), block_dim: (threads, 1, 1), shared_mem_bytes: 0 }
355}
356
357impl CudaBackend {
358    fn tensor<'w>(&self, w: &'w Weights, i: usize) -> Result<&'w Tensor> {
359        w.0.get(i).ok_or_else(|| Error::Unsupported(format!("weight {i} is not in the checkpoint")))
360    }
361
362    fn ptr32(&self, w: &Weights, i: usize) -> Result<u64> {
363        Ok(self.tensor(w, i)?.f32.device_ptr(&self.stream).0)
364    }
365
366    /// The FP16 copy of weight `i`, made on first use.
367    fn ptr16(&self, w: &Weights, i: usize) -> Result<u64> {
368        let t = self.tensor(w, i)?;
369        if let Some(h) = t.f16.get() {
370            return Ok(h.device_ptr(&self.stream).0);
371        }
372        let len = t.f32.len();
373        // SAFETY: every element is written by the kernel below before anything reads it.
374        let mut h = unsafe { self.stream.alloc::<u16>(len) }.map_err(dev)?;
375        let blocks = len.div_ceil(256).min(65_535) as u32;
376        let cfg =
377            LaunchConfig { grid_dim: (blocks, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 };
378        let n = len as u64;
379        let mut l = self.stream.launch_builder(&self.k.to_f16);
380        l.arg(&mut h).arg(&t.f32).arg(&n);
381        // SAFETY: to_f16 reads len floats and writes len halves, the sizes of both buffers.
382        unsafe { l.launch(cfg) }.map_err(dev)?;
383        Ok(t.f16.get_or_init(|| h).device_ptr(&self.stream).0)
384    }
385
386    fn launch_step(&self, p: &CudaPlan, step: &Step) -> Result<()> {
387        let b = p.bucket;
388        let Ptrs { n, ids, pos, seq, cu, mcu, mrow, qtype, tile } = p.ptrs;
389        let s = &self.stream;
390        // SAFETY for every launch below: lowering checked that each value's arena range holds its
391        // bucket sized rows at its type, each kernel touches rows below the batch's real count
392        // only, and the index tables are sized for the bucket too.
393        match *step {
394            Step::Embed { table, out } => {
395                let d = out.width as i32;
396                let mut l = s.launch_builder(&self.k.embed);
397                l.arg(&out.ptr).arg(&table).arg(&ids).arg(&n).arg(&d);
398                // SAFETY: see above.
399                unsafe { l.launch(rows(b.tokens, 256)) }.map_err(dev)?;
400            }
401            Step::LayerNorm { x, w, b: bias, eps, out } => {
402                let f = &self.k.ln[2 * usize::from(x.half()) + usize::from(out.half())];
403                let (k, d) = (kind(x.rows), x.width as i32);
404                let mut l = s.launch_builder(f);
405                l.arg(&out.ptr).arg(&x.ptr).arg(&w).arg(&bias).arg(&n).arg(&k).arg(&d).arg(&eps);
406                let cfg = LaunchConfig {
407                    grid_dim: (b.rows(x.rows).div_ceil(LN_ROWS) as u32, 1, 1),
408                    block_dim: (32 * LN_ROWS as u32, 1, 1),
409                    shared_mem_bytes: 0,
410                };
411                // SAFETY: see above.
412                unsafe { l.launch(cfg) }.map_err(dev)?;
413            }
414            Step::ToHalf { x, len } => {
415                let blocks = len.div_ceil(256).min(65_535) as u32;
416                let cfg = LaunchConfig {
417                    grid_dim: (blocks, 1, 1),
418                    block_dim: (256, 1, 1),
419                    shared_mem_bytes: 0,
420                };
421                let mut l = s.launch_builder(&self.k.to_f16);
422                l.arg(&p.stage).arg(&x).arg(&len);
423                // SAFETY: see above, and lowering sized the staging buffer for the largest one.
424                unsafe { l.launch(cfg) }.map_err(dev)?;
425            }
426            Step::Gemm { gemm, b: bias, act, out } => {
427                let ws = self.workspace_ptr();
428                // SAFETY: see above, and the workspace is used by nothing else on the stream.
429                unsafe { p.gemms[gemm].run(&self.lt, ws, WORKSPACE, s.cu_stream().cast()) }?;
430                if bias != 0 || act != 0 {
431                    let f = &self.k.bias_act[usize::from(out.half())];
432                    let (k, w) = (kind(out.rows), out.width as i32);
433                    let mut l = s.launch_builder(f);
434                    l.arg(&out.ptr).arg(&bias).arg(&n).arg(&k).arg(&w).arg(&act);
435                    // SAFETY: see above.
436                    unsafe { l.launch(rows(b.rows(out.rows), 256)) }.map_err(dev)?;
437                }
438            }
439            Step::Rope { qkv, cos, sin } => {
440                let heads = (qkv.width / 3 / HEAD) as i32;
441                let mut l = s.launch_builder(&self.k.rope[usize::from(qkv.half())]);
442                l.arg(&qkv.ptr).arg(&cos).arg(&sin).arg(&pos).arg(&n).arg(&heads);
443                // SAFETY: see above.
444                unsafe { l.launch(rows(b.tokens, 256)) }.map_err(dev)?;
445            }
446            Step::Attention { qkv, cos, sin, window, out } => {
447                let heads = (out.width / HEAD) as i32;
448                let mut l = s.launch_builder(
449                    &self.k.attention[2 * usize::from(qkv.half()) + usize::from(out.half())],
450                );
451                l.arg(&out.ptr).arg(&qkv.ptr).arg(&cos).arg(&sin).arg(&pos);
452                l.arg(&seq).arg(&cu).arg(&tile).arg(&n);
453                l.arg(&heads).arg(&window);
454                let cfg = LaunchConfig {
455                    grid_dim: (att_blocks(b) as u32, heads as u32, 1),
456                    block_dim: (32 * ATT_W, 1, 1),
457                    shared_mem_bytes: 0,
458                };
459                // SAFETY: see above.
460                unsafe { l.launch(cfg) }.map_err(dev)?;
461            }
462            Step::GeGlu { x, out } => {
463                let inter = out.width as i32;
464                let mut l = s.launch_builder(
465                    &self.k.geglu[2 * usize::from(x.half()) + usize::from(out.half())],
466                );
467                l.arg(&out.ptr).arg(&x.ptr).arg(&n).arg(&inter);
468                // SAFETY: see above.
469                unsafe { l.launch(rows(b.tokens, 256)) }.map_err(dev)?;
470            }
471            Step::AddType { h, table } => {
472                let d = h.width as i32;
473                let mut l = s.launch_builder(&self.k.add_type);
474                l.arg(&h.ptr).arg(&table).arg(&seq).arg(&qtype).arg(&n).arg(&d);
475                // SAFETY: see above.
476                unsafe { l.launch(rows(b.tokens, 256)) }.map_err(dev)?;
477            }
478            Step::Gather { h, out } => {
479                let d = h.width as i32;
480                let mut l = s.launch_builder(&self.k.gather[usize::from(h.half())]);
481                l.arg(&out.ptr).arg(&h.ptr).arg(&mrow).arg(&n).arg(&d);
482                // SAFETY: see above.
483                unsafe { l.launch(rows(b.markers, 256)) }.map_err(dev)?;
484            }
485            Step::ActFeatures { h, logits, out } => {
486                let d = h.width as i32;
487                let mut l = s.launch_builder(&self.k.act_features);
488                l.arg(&out.ptr).arg(&h.ptr).arg(&logits.ptr).arg(&cu).arg(&mcu);
489                l.arg(&n).arg(&d);
490                // SAFETY: see above.
491                unsafe { l.launch(rows(b.seqs, 256)) }.map_err(dev)?;
492            }
493        }
494        Ok(())
495    }
496
497    /// Enqueues the copy of the index tables to the device.
498    fn copy_in(&self, p: &CudaPlan) -> Result<()> {
499        let src = p.index_host.as_slice().map_err(dev)?;
500        // SAFETY: the device table holds `at.len` u32 values, as many as the host one, and both
501        // live as long as the plan, which outlives every run and the graph.
502        unsafe { result::memcpy_htod_async(p.ptrs.n, src, self.stream.cu_stream()) }.map_err(dev)
503    }
504
505    /// Enqueues the copy of the bucket's logits and act outputs to the host.
506    fn copy_out(&self, p: &mut CudaPlan) -> Result<()> {
507        let (lp, ap, m) = (p.logits.ptr, p.act.ptr, p.bucket.markers);
508        let cu = self.stream.cu_stream();
509        let dst = p.host_out.as_mut_slice().map_err(dev)?;
510        let (logits, act) = dst.split_at_mut(m);
511        // SAFETY: lowering sized the host buffer for one logit per marker and two act values per
512        // sequence of the bucket, the sizes of the two arena values, and it lives as the plan does.
513        unsafe {
514            result::memcpy_dtoh_async(logits, lp, cu).map_err(dev)?;
515            result::memcpy_dtoh_async(act, ap, cu).map_err(dev)
516        }
517    }
518
519    /// Captures a whole run as one graph.
520    fn capture(&self, p: &mut CudaPlan) -> Result<Captured> {
521        let s = &self.stream;
522        // Relaxed, because the pinned buffers wait on their own event, never recorded, before they
523        // hand out their pointers, and a stricter mode refuses any wait during a capture.
524        s.begin_capture(sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED).map_err(dev)?;
525        let mut enqueue = || {
526            self.copy_in(p)?;
527            for step in &p.steps {
528                self.launch_step(p, step)?;
529            }
530            self.copy_out(p)
531        };
532        let queued = enqueue();
533        let flags = sys::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH;
534        let graph = s.end_capture(flags).map_err(dev)?;
535        queued?;
536        let graph = graph.ok_or_else(|| Error::Device("graph capture recorded nothing".into()))?;
537        graph.upload().map_err(dev)?;
538        Ok(Captured(graph))
539    }
540}
541
542impl Backend for CudaBackend {
543    type Weights = Weights;
544    type Plan = CudaPlan;
545
546    fn caps(&self) -> Caps {
547        Caps { name: "cuda", threads: 1, graphs: true, unified_memory: false }
548    }
549
550    fn weight_bytes(&self, w: &Weights) -> usize {
551        w.0.iter().map(|t| 4 * t.f32.len() + t.f16.get().map_or(0, |h| 2 * h.len())).sum()
552    }
553
554    fn plan_bytes(&self, p: &CudaPlan) -> usize {
555        p.arena.len() + 2 * p._stage.len()
556    }
557
558    fn upload(&self, tensors: &[HostTensor<'_>], _graph: &Graph) -> Result<Weights> {
559        let mut out = Vec::with_capacity(tensors.len());
560        let mut host = Vec::new();
561        for (i, h) in tensors.iter().enumerate() {
562            let n = h.bytes.len() / h.dtype.size();
563            if n != h.shape.iter().product::<usize>() {
564                return Err(Error::Unsupported(format!(
565                    "tensor {i} has {n} values for {:?}",
566                    h.shape
567                )));
568            }
569            host.clear();
570            host.extend((0..n).map(|j| h.dtype.read_f32(h.bytes, j)));
571            let f32 = self.stream.clone_htod(&host).map_err(dev)?;
572            out.push(Tensor { shape: h.shape.to_vec(), f32, f16: OnceLock::new() });
573        }
574        self.stream.synchronize().map_err(dev)?;
575        Ok(Weights(out))
576    }
577
578    #[allow(clippy::too_many_lines)]
579    fn lower(&self, w: &Weights, graph: &Graph, bucket: Bucket) -> Result<CudaPlan> {
580        let lay = layout(graph, |r| bucket.rows(r));
581        let ty = types(graph, self.precision);
582        let arena = self.stream.alloc_zeros::<u8>(4 * lay.len.max(1)).map_err(dev)?;
583        let base = arena.device_ptr(&self.stream).0;
584        let loc = |v: Val| {
585            let s = graph.shape(v);
586            let i = v.0 as usize;
587            Loc { ptr: base + 4 * lay.offsets[i] as u64, rows: s.rows, width: s.width, ty: ty[i] }
588        };
589        let bad = |m: String| Err(Error::Unsupported(m));
590        let shape = |i: usize| self.tensor(w, i).map(|t| t.shape.as_slice());
591        let bias = |b: Option<usize>| b.map_or(Ok(0), |b| self.ptr32(w, b));
592        let mut ropes: Vec<(u64, CudaSlice<f32>, CudaSlice<f32>)> = Vec::new();
593        let stage_len = graph
594            .ops
595            .iter()
596            .filter_map(|op| match *op {
597                Op::Gemm { a, out, .. }
598                    if ty[a.0 as usize] == Ty::F32 && ty[out.0 as usize] == Ty::F16 =>
599                {
600                    let s = graph.shape(a);
601                    Some(bucket.rows(s.rows) * s.width)
602                }
603                _ => None,
604            })
605            .max()
606            .unwrap_or(0);
607        let stage = self.stream.alloc_zeros::<u16>(stage_len.max(1)).map_err(dev)?;
608        let stage_base = stage.device_ptr(&self.stream).0;
609        let (mut gemms, mut keys) = (Vec::new(), Vec::new());
610        let mut steps = Vec::with_capacity(graph.ops.len());
611        let mut fused = None;
612        for (i, op) in graph.ops.iter().enumerate() {
613            let step = match *op {
614                Op::Embed { table, out } => {
615                    let out = loc(out);
616                    if shape(table)?.get(1) != Some(&out.width) || out.rows != Rows::Tokens {
617                        return bad(format!("op {i}: embedding table does not match its output"));
618                    }
619                    Step::Embed { table: self.ptr32(w, table)?, out }
620                }
621                Op::LayerNorm { x, w: nw, b, eps, out } => {
622                    let (x, out) = (loc(x), loc(out));
623                    let ok = shape(nw)? == [x.width]
624                        && b.map_or(Ok(true), |b| shape(b).map(|s| s == [x.width]))?
625                        && x.width == out.width
626                        && x.width <= 1024
627                        && x.rows == out.rows;
628                    if !ok {
629                        return bad(format!(
630                            "op {i}: layer norm shapes do not match or are over 1024"
631                        ));
632                    }
633                    let eps = eps as f32;
634                    Step::LayerNorm { x, w: self.ptr32(w, nw)?, b: bias(b)?, eps, out }
635                }
636                Op::Gemm { a, w: gw, b, epilogue, out } => {
637                    let (a, out) = (loc(a), loc(out));
638                    let ok = shape(gw)? == [out.width, a.width]
639                        && b.map_or(Ok(true), |b| shape(b).map(|s| s == [out.width]))?
640                        && a.rows == out.rows;
641                    if !ok {
642                        return bad(format!("op {i}: gemm shapes do not match"));
643                    }
644                    let m = bucket.rows(a.rows);
645                    if m == 0 {
646                        continue;
647                    }
648                    let mut x = a.ptr;
649                    let (wp, ab) = match (a.ty, out.ty) {
650                        (Ty::F32, Ty::F32) => (self.ptr32(w, gw)?, Ty::F32),
651                        (Ty::F32, Ty::F16) => {
652                            let len = m * a.width;
653                            steps.push(Step::ToHalf { x, len: len as u64 });
654                            x = stage_base;
655                            (self.ptr16(w, gw)?, Ty::F16)
656                        }
657                        (Ty::F16, _) => (self.ptr16(w, gw)?, Ty::F16),
658                    };
659                    let (act, acc) = match epilogue {
660                        Epilogue::None => (0, false),
661                        Epilogue::Gelu => (1, false),
662                        Epilogue::Relu => (2, false),
663                        Epilogue::Accumulate => (0, true),
664                    };
665                    // Picks are per inner size, width and types, not rows, for the reason
666                    // RANKED_ROWS gives.
667                    let dims = (lt::RANKED_ROWS, a.width, out.width);
668                    let key = Key { dims, ab, c: out.ty, acc };
669                    let g = lt::Gemm::new(
670                        &self.lt,
671                        (m, a.width, out.width),
672                        ab,
673                        out.ty,
674                        acc,
675                        WORKSPACE,
676                        (wp, x, out.ptr),
677                        self.picks.get(&key),
678                    )?;
679                    gemms.push(g);
680                    keys.push(key);
681                    Step::Gemm { gemm: gemms.len() - 1, b: bias(b)?, act, out }
682                }
683                Op::Rope { qkv, theta } => {
684                    let qkv = loc(qkv);
685                    if !qkv.width.is_multiple_of(3 * HEAD) || qkv.rows != Rows::Tokens {
686                        return bad(format!("op {i}: rope needs token rows of 3 heads 64"));
687                    }
688                    let at = match ropes.iter().position(|r| r.0 == theta.to_bits()) {
689                        Some(at) => at,
690                        None => {
691                            let (c, s) = rope_tables(theta, bucket.tokens.max(1));
692                            let c = self.stream.clone_htod(&c).map_err(dev)?;
693                            let s = self.stream.clone_htod(&s).map_err(dev)?;
694                            ropes.push((theta.to_bits(), c, s));
695                            ropes.len() - 1
696                        }
697                    };
698                    let cos = ropes[at].1.device_ptr(&self.stream).0;
699                    let sin = ropes[at].2.device_ptr(&self.stream).0;
700                    // Attention right after on the same rows rotates as it loads, which saves
701                    // writing q and k back and reading them again.
702                    if matches!(graph.ops.get(i + 1), Some(Op::Attention { qkv: v, .. }) if loc(*v).ptr == qkv.ptr)
703                    {
704                        fused = Some((qkv.ptr, cos, sin));
705                        continue;
706                    }
707                    Step::Rope { qkv, cos, sin }
708                }
709                Op::Attention { qkv, window, out } => {
710                    let (qkv, out) = (loc(qkv), loc(out));
711                    let ok = qkv.width.is_multiple_of(3 * HEAD)
712                        && out.width * 3 == qkv.width
713                        && qkv.rows == Rows::Tokens
714                        && out.rows == Rows::Tokens;
715                    if !ok {
716                        return bad(format!("op {i}: attention shapes or types do not match"));
717                    }
718                    let window = window.map_or(Ok(-1), |w| {
719                        i32::try_from(w).map_err(|_| Error::Unsupported(format!("op {i}: window")))
720                    })?;
721                    let (cos, sin) = match fused.take() {
722                        Some((p, cos, sin)) if p == qkv.ptr => (cos, sin),
723                        _ => (0, 0),
724                    };
725                    Step::Attention { qkv, cos, sin, window, out }
726                }
727                Op::GeGlu { x, out } => {
728                    let (x, out) = (loc(x), loc(out));
729                    if x.width != 2 * out.width || x.rows != out.rows {
730                        return bad(format!("op {i}: geglu needs an input twice its output"));
731                    }
732                    Step::GeGlu { x, out }
733                }
734                Op::AddType { h, table } => {
735                    let h = loc(h);
736                    if shape(table)?.get(1) != Some(&h.width) || h.rows != Rows::Tokens {
737                        return bad(format!("op {i}: type table does not match"));
738                    }
739                    Step::AddType { h, table: self.ptr32(w, table)? }
740                }
741                Op::GatherMarkers { h, out } => {
742                    let (h, out) = (loc(h), loc(out));
743                    let ok = h.width == out.width
744                        && h.rows == Rows::Tokens
745                        && out.rows == Rows::Markers
746                        && h.ty == out.ty;
747                    if !ok {
748                        return bad(format!("op {i}: gather shapes do not match"));
749                    }
750                    Step::Gather { h, out }
751                }
752                Op::ActFeatures { h, logits, out } => {
753                    let (h, logits, out) = (loc(h), loc(logits), loc(out));
754                    let ok = out.width == h.width + 4
755                        && logits.width == 1
756                        && logits.rows == Rows::Markers
757                        && out.rows == Rows::Seqs
758                        && !h.half()
759                        && !logits.half();
760                    if !ok {
761                        return bad(format!("op {i}: act feature shapes do not match"));
762                    }
763                    Step::ActFeatures { h, logits, out }
764                }
765                Op::MeanPool { .. } => return bad(format!("op {i}: mean pool is not on CUDA yet")),
766            };
767            steps.push(step);
768        }
769        let (Some(logits), Some(act)) = (graph.logits, graph.act) else {
770            return bad("the graph has no logits or act output".into());
771        };
772        let (logits, act) = (loc(logits), loc(act));
773        if logits.width != 1
774            || logits.rows != Rows::Markers
775            || act.width != 2
776            || act.rows != Rows::Seqs
777        {
778            return bad("outputs are not one logit per marker and two per sequence".into());
779        }
780        let at = Index::new(bucket);
781        let index = self.stream.alloc_zeros::<u32>(at.len).map_err(dev)?;
782        let ptrs = Ptrs::new(index.device_ptr(&self.stream).0, at);
783        let ctx = self.stream.context();
784        // SAFETY: both buffers are zeroed below before anything reads them.
785        let (mut index_host, mut host_out) = unsafe {
786            (
787                ctx.alloc_pinned::<u32>(at.len).map_err(dev)?,
788                ctx.alloc_pinned::<f32>(bucket.markers + 2 * bucket.seqs).map_err(dev)?,
789            )
790        };
791        index_host.as_mut_slice().map_err(dev)?.fill(0);
792        host_out.as_mut_slice().map_err(dev)?.fill(0.0);
793        self.stream.synchronize().map_err(dev)?;
794        let mut plan = CudaPlan {
795            bucket,
796            steps,
797            gemms,
798            arena,
799            _stage: stage,
800            stage: stage_base,
801            _ropes: ropes,
802            _index: index,
803            index_host,
804            at,
805            ptrs,
806            logits,
807            act,
808            host_out,
809            graph: None,
810            profile: None,
811            profiled: 0,
812        };
813        if tune::enabled() {
814            let ws = self.workspace_ptr();
815            tune::tune(&self.lt, &self.stream, ws, &mut plan.gemms, &keys, &self.name)?;
816        }
817        plan.graph = Some(self.capture(&mut plan)?);
818        Ok(plan)
819    }
820
821    fn run(&self, p: &mut CudaPlan, batch: &Batch<'_>, out: &mut Outputs) -> Result<()> {
822        let (t, s, m) = (batch.ids.len(), batch.seqs(), batch.markers.len());
823        let at = p.at;
824        let h = p.index_host.as_mut_slice().map_err(dev)?;
825        let blocks = att_tiles(batch.cu, &mut h[at.tile..at.tile + att_blocks(p.bucket)]);
826        h[..4].copy_from_slice(&[t as u32, s as u32, m as u32, blocks as u32]);
827        h[at.ids..at.ids + t].copy_from_slice(batch.ids);
828        h[at.cu..=at.cu + s].copy_from_slice(batch.cu);
829        h[at.mcu..=at.mcu + s].copy_from_slice(batch.mcu);
830        for q in 0..s {
831            let (lo, hi) = (batch.cu[q] as usize, batch.cu[q + 1] as usize);
832            for r in lo..hi {
833                h[at.pos + r] = (r - lo) as u32;
834                h[at.seq + r] = q as u32;
835            }
836            for k in batch.mcu[q] as usize..batch.mcu[q + 1] as usize {
837                h[at.mrow + k] = lo as u32 + batch.markers[k];
838            }
839            h[at.qtype + q] = u32::from(batch.qtype[q]);
840        }
841        if p.profile.is_some() {
842            self.copy_in(p)?;
843            self.stream.synchronize().map_err(dev)?;
844            for i in 0..p.steps.len() {
845                let t = std::time::Instant::now();
846                self.launch_step(p, &p.steps[i])?;
847                self.stream.synchronize().map_err(dev)?;
848                let ns = t.elapsed().as_nanos() as u64;
849                if let Some(v) = p.profile.as_mut() {
850                    v[i] += ns;
851                }
852            }
853            self.copy_out(p)?;
854            p.profiled += 1;
855        } else if let Some(g) = &p.graph {
856            g.0.launch().map_err(dev)?;
857        }
858        self.stream.synchronize().map_err(dev)?;
859        let host = p.host_out.as_slice().map_err(dev)?;
860        let (logits, act) = host.split_at(p.bucket.markers);
861        out.logits.clear();
862        out.logits.extend_from_slice(&logits[..m]);
863        out.act.clear();
864        out.act.extend(act[..2 * s].as_chunks::<2>().0.iter().copied());
865        Ok(())
866    }
867}
868
869/// Attention blocks a bucket launches: enough for every sequence to start its own.
870pub(crate) fn att_blocks(b: Bucket) -> usize {
871    b.tokens.div_ceil(ATT_Q) + b.seqs
872}
873
874/// Writes the first row of each attention block to `out` and returns how many there are. Blocks
875/// start at every `ATT_Q`th row of each sequence, so the rows a block holds and the key tiles it
876/// walks depend only on positions within its sequence, never on where the sequence sits in the
877/// batch, and a row gets the same bits alone or batched.
878pub(crate) fn att_tiles(cu: &[u32], out: &mut [u32]) -> usize {
879    let mut k = 0;
880    for w in cu.windows(2) {
881        for i in (w[0]..w[1]).step_by(ATT_Q) {
882            out[k] = i;
883            k += 1;
884        }
885    }
886    k
887}
888
889/// cos and sin tables `[len, 32]` for heads of 64, computed as the CPU backend does.
890pub(crate) fn rope_tables(theta: f64, len: usize) -> (Vec<f32>, Vec<f32>) {
891    let half = HEAD / 2;
892    let inv: Vec<f32> = (0..half)
893        .map(|i| {
894            let e = (2 * i) as f32 / HEAD as f32;
895            1.0 / (theta.powf(f64::from(e)) as f32)
896        })
897        .collect();
898    let mut cos = Vec::with_capacity(len * half);
899    let mut sin = Vec::with_capacity(len * half);
900    for p in 0..len {
901        for &f in &inv {
902            let a = f64::from(p as f32 * f);
903            cos.push(a.cos() as f32);
904            sin.push(a.sin() as f32);
905        }
906    }
907    (cos, sin)
908}