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