spark_model/lora/
overlay_tables.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Token-overlay device pointer tables (Feature 2). The per-adapter-slot
4//! overlays are addressed by `[max_loras]` device tables of pointers/counts at
5//! LOAD-TIME-FIXED addresses, exactly like the attention BGMV `a_table`/
6//! `b_table` (`ops::lora_delta::LoraRoute`): the only per-step kernel argument
7//! is `seq_slot` (device i32, contents re-uploaded each step), so the tables are
8//! stable kernel args across CUDA-graph capture/replay and the overlay launches
9//! capture cleanly. Cell `k == 0` (null pointer) or `n_override[k] == 0` ⇒ that
10//! slot's overlay is skipped by the kernel — zero-overhead for base requests.
11
12use anyhow::Result;
13use spark_runtime::gpu::{DevicePtr, GpuBackend};
14
15use super::overlay_build::EmbedOverlay;
16
17/// The resolved overlay set for the whole adapter pool. Built once in
18/// `set_lora_weights` (Stage 2) from the per-slot [`EmbedOverlay`]s. `None` on
19/// the model ⇒ overlay feature OFF ⇒ every forward hook early-returns
20/// (byte-identical to a no-overlay build).
21pub struct TokenOverlaySet {
22    /// Per-slot compact overlays (`len == max_loras`), retained so the device
23    /// buffers they own outlive the tables that point into them.
24    pub overlays: Vec<Option<EmbedOverlay>>,
25    /// `u64[max_loras]` → `i32*[vocab]` embed slot_map (0 = slot has no overlay).
26    pub embed_slot_map_table: DevicePtr,
27    /// `u64[max_loras]` → `bf16*[n,h]` embed override rows.
28    pub embed_rows_table: DevicePtr,
29    /// `u32[max_loras]` EMBED n_override per slot — the row count of each
30    /// slot's `embed_rows_table` cell, the embed kernel's `slot < n` bound
31    /// (CWE-125 guard). Distinct from `n_override_table`, which is the
32    /// LM_HEAD count (0 for an untied slot with no lm_head overlay).
33    pub embed_n_table: DevicePtr,
34    /// `slot_map` length shared by every resident overlay (each slot records
35    /// the served vocab it was built against; `from_slots` REFUSES a mix) —
36    /// the embed kernel's `ids[r] < vocab` bound (CWE-125 guard).
37    pub vocab: u32,
38    /// `u64[max_loras]` → `bf16*[n,h]` lm_head override rows (== embed cell when tied).
39    pub lmhead_rows_table: DevicePtr,
40    /// `u64[max_loras]` → `u32*[n]` lm_head override ids (== embed cell when tied).
41    pub lmhead_ids_table: DevicePtr,
42    /// `u32[max_loras]` lm_head n_override per slot (0 ⇒ lm_head skip).
43    pub n_override_table: DevicePtr,
44    /// `max(lmhead n_override)` across slots — the lm_head kernel's `grid.y`.
45    pub max_n_override: u32,
46}
47
48/// Pack a `[max_loras]` u64 pointer array to device (le bytes → alloc → h2d),
49/// mirroring the `mk` closure in `loading.rs`.
50fn mk_u64(gpu: &dyn GpuBackend, tab: &[u64]) -> Result<DevicePtr> {
51    let bytes: Vec<u8> = tab.iter().flat_map(|p| p.to_le_bytes()).collect();
52    let d = gpu.alloc(bytes.len())?;
53    gpu.copy_h2d(&bytes, d)?;
54    Ok(d)
55}
56
57fn mk_u32(gpu: &dyn GpuBackend, tab: &[u32]) -> Result<DevicePtr> {
58    let bytes: Vec<u8> = tab.iter().flat_map(|p| p.to_le_bytes()).collect();
59    let d = gpu.alloc(bytes.len())?;
60    gpu.copy_h2d(&bytes, d)?;
61    Ok(d)
62}
63
64impl TokenOverlaySet {
65    /// Build the device tables from per-slot overlays. `tied` ⇒ the lm_head
66    /// reuses each slot's embed override rows/ids (tied vocab head, or a
67    /// quantized head derived from embed): the logit recompute `dot(hidden,
68    /// embed_row)` is exactly the tied output projection. An untied slot that
69    /// ships its own lm_head overlay uses its distinct rows/ids; an untied slot
70    /// that does NOT ⇒ `n_override[k] = 0` (embed-only correction).
71    pub fn from_slots(
72        gpu: &dyn GpuBackend,
73        overlays: Vec<Option<EmbedOverlay>>,
74        max_loras: usize,
75        tied: bool,
76    ) -> Result<Self> {
77        let mut slot_map_tab = vec![0u64; max_loras];
78        let mut embed_rows_tab = vec![0u64; max_loras];
79        let mut embed_n_tab = vec![0u32; max_loras];
80        let mut lmhead_rows_tab = vec![0u64; max_loras];
81        let mut lmhead_ids_tab = vec![0u64; max_loras];
82        let mut n_override_tab = vec![0u32; max_loras];
83        let mut max_n_override = 0u32;
84        let mut vocab = 0u32;
85
86        for (k, ov) in overlays.iter().enumerate() {
87            let Some(ov) = ov else { continue };
88            slot_map_tab[k] = ov.slot_map.0;
89            embed_rows_tab[k] = ov.rows.0;
90            embed_n_tab[k] = ov.n_override;
91            // One build pass sizes every slot_map to the same served vocab; a
92            // mix would make the kernel's single `ids[r] < vocab` bound wrong
93            // for some slot, so REFUSE it rather than guess a bound.
94            anyhow::ensure!(
95                vocab == 0 || vocab == ov.vocab,
96                "token-overlay: slot {k} was built against vocab {} but an \
97                 earlier slot against {vocab}; refusing mixed-vocab overlay tables",
98                ov.vocab
99            );
100            vocab = ov.vocab;
101            let (rows, ids, n) = match (&ov.lmhead, tied) {
102                (Some(lm), _) => (lm.rows.0, lm.ids_dev.0, lm.n_override),
103                (None, true) => (ov.rows.0, ov.ids_dev.0, ov.n_override),
104                (None, false) => (0, 0, 0),
105            };
106            lmhead_rows_tab[k] = rows;
107            lmhead_ids_tab[k] = ids;
108            n_override_tab[k] = n;
109            max_n_override = max_n_override.max(n);
110        }
111
112        Ok(Self {
113            embed_slot_map_table: mk_u64(gpu, &slot_map_tab)?,
114            embed_rows_table: mk_u64(gpu, &embed_rows_tab)?,
115            embed_n_table: mk_u32(gpu, &embed_n_tab)?,
116            vocab,
117            lmhead_rows_table: mk_u64(gpu, &lmhead_rows_tab)?,
118            lmhead_ids_table: mk_u64(gpu, &lmhead_ids_tab)?,
119            n_override_table: mk_u32(gpu, &n_override_tab)?,
120            max_n_override,
121            overlays,
122        })
123    }
124
125    /// True when at least one slot has a resident overlay (else the set is inert
126    /// and the caller can drop it to keep the hooks byte-identical to off).
127    pub fn any_active(&self) -> bool {
128        self.overlays.iter().any(|o| o.is_some())
129    }
130}
131
132#[cfg(test)]
133#[path = "overlay_tables_tests.rs"]
134mod tests;