metrale_model_layers/lora/slot_math.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: GPU-free LoRA slot math: victim selection for the promotion
4//! cache, the per-row `seq_slot` buffer, scale-table values, the routed-prefill
5//! slot and pair, and the per-slot byte layout of the adapter pool.
6//!
7//! Owner: model-layers (lora).
8//! Invariants:
9//! - `pool_slot_bytes`, `module_slot_offsets` and `pack_slot` (`loading.rs`)
10//! walk one layout: layers ascending, then `LoraModule::ALL` filtered by
11//! `applies_to_layer`, A (`max_rank * in` elements) then B
12//! (`out * max_rank`), BF16.
13
14use metrale_config::ModelConfig;
15
16use super::*;
17use crate::layers::ops::lora_delta::LoraPair;
18
19pub(crate) const BF16_BYTES: usize = 2;
20
21/// 2026-09-25: Pick the slot a promotion overwrites, from `cache`, the
22/// `(slot_index, view)` pairs the caller passes (model-engine passes
23/// `cache_slot_views()`, slots `[pinned, max_loras)`):
24/// 1. the first unfilled slot;
25/// 2. else the `ref_count == 0` slot with the smallest `last_used`;
26/// 3. else `Err(PoolFull)`.
27pub fn select_victim_slot(cache: &[(usize, SlotView)]) -> Result<usize, VictimError> {
28 if let Some((idx, _)) = cache.iter().find(|(_, v)| !v.filled) {
29 return Ok(*idx);
30 }
31 cache
32 .iter()
33 .filter(|(_, v)| v.ref_count == 0)
34 .min_by_key(|(_, v)| v.last_used)
35 .map(|(idx, _)| *idx)
36 .ok_or(VictimError::PoolFull)
37}
38
39/// 2026-09-25: The host `seq_slot[padded_n]` buffer the bgmv reads, one entry
40/// per row:
41/// row i < n: `adapter_slots[i]` if `>= 0`, else `active` (a negative slot
42/// means the active adapter);
43/// row i >= n (padding): `-1`, which `lora_bgmv.cu` treats as no delta.
44pub fn build_seq_slot_host(adapter_slots: &[i32], padded_n: usize, active: i32) -> Vec<i32> {
45 let n = adapter_slots.len();
46 (0..padded_n)
47 .map(|i| {
48 if i < n {
49 let s = adapter_slots[i];
50 if s >= 0 { s } else { active }
51 } else {
52 -1
53 }
54 })
55 .collect()
56}
57
58/// 2026-10-03: Which attention LoRA sites a step of one sequence runs.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum LoraSites {
61 /// 2026-10-03: Sites that fold the installed pair when no slot buffer is given: one-row decode
62 /// (`apply_q_lora`, `apply_kv_lora`, the o_proj fold) and prefill (`prefill_one_proj`,
63 /// `paged_oproj`).
64 PairFallback,
65 /// 2026-10-03: The multi-sequence sites an MTP verify's K rows run (`ms_qkv_apply_lora`,
66 /// `ms_o_proj_lora`): they have no pair fallback and fold nothing without a slot buffer.
67 MultiSeq,
68}
69
70/// 2026-10-03: The `count`-row slot buffer a step of one sequence uploads, or `None` for the
71/// installed-pair path: only a request that resolves to the active adapter, on sites with a pair
72/// fallback, takes the pair. Multi-sequence sites always get their rows' slots, or the attention
73/// would run without the adapter (the active-adapter MTP verify did until 2026-10-03).
74pub fn uniform_seq_slots(
75 adapter_slot: i32,
76 active: i32,
77 count: usize,
78 sites: LoraSites,
79) -> Option<Vec<i32>> {
80 let resolved = if adapter_slot >= 0 {
81 adapter_slot
82 } else {
83 active
84 };
85 if sites == LoraSites::PairFallback && resolved == active {
86 return None;
87 }
88 Some(build_seq_slot_host(
89 &vec![adapter_slot; count],
90 count,
91 active,
92 ))
93}
94
95/// 2026-09-25: Values of the `[max_loras]` f32 scale table: entry `k` is
96/// adapter `k`'s `scaling()` (alpha/r, or alpha/√r under rsLoRA), 0.0 for
97/// `k >= adapters.len()`.
98pub(crate) fn scale_table_values(adapters: &[LoraAdapterInput<'_>], max_loras: usize) -> Vec<f32> {
99 let mut v = vec![0.0f32; max_loras];
100 for (k, a) in adapters.iter().enumerate() {
101 v[k] = a.peft.scaling();
102 }
103 v
104}
105
106/// 2026-09-25: The predicate behind [`LoraWeights::routed_prefill_slot`].
107/// Resolves `adapter_slot` (`>= 0` is that slot, negative is `active`) and
108/// returns it only when it is not `active` and is `< num_slots`; otherwise
109/// `None`.
110pub fn routed_prefill_slot_of(adapter_slot: i32, active: usize, num_slots: usize) -> Option<usize> {
111 let resolved = if adapter_slot >= 0 {
112 adapter_slot as usize
113 } else {
114 active
115 };
116 (resolved != active && resolved < num_slots).then_some(resolved)
117}
118
119/// 2026-09-25: The [`LoraPair`] for (global layer, module) in a slot's
120/// global-layer-indexed `layers`. `None` when the index is out of range, the
121/// layer has no pairs, or the adapter has no pair for that module.
122pub fn select_routed_pair(
123 layers: &[Option<LoraLayerWeights>],
124 global_layer_idx: usize,
125 module: LoraModule,
126) -> Option<&LoraPair> {
127 layers
128 .get(global_layer_idx)
129 .and_then(|o| o.as_ref())
130 .and_then(|l| l.module_pair(module))
131}
132
133/// 2026-09-25: Bytes of one pool slot: over every layer and every module that
134/// `applies_to_layer`, `(max_rank * in + out * max_rank) * 2`.
135pub(crate) fn pool_slot_bytes(cfg: &ModelConfig, max_rank: usize) -> usize {
136 (0..cfg.num_hidden_layers)
137 .map(|layer| {
138 LoraModule::ALL
139 .iter()
140 .filter(|m| m.applies_to_layer(cfg, layer))
141 .map(|m| {
142 let (out, inp) = m.dims(cfg);
143 (max_rank * inp + out * max_rank) * BF16_BYTES
144 })
145 .sum::<usize>()
146 })
147 .sum()
148}
149
150/// 2026-09-25: Byte offset of slot `slot` in the pool:
151/// `slot * pool_slot_bytes`.
152// 2026-09-25: Its callers are `rdma_stage` and tests; `rdma_stage` compiles only
153// on unix, with `cuda` or under test (`mod.rs`).
154#[cfg_attr(not(all(feature = "cuda", unix)), allow(dead_code))]
155pub(crate) fn slot_base_offset(slot: usize, cfg: &ModelConfig, max_rank: usize) -> usize {
156 slot * pool_slot_bytes(cfg, max_rank)
157}
158
159/// 2026-09-25: The (a_off, b_off) of (layer, module) within a slot: the
160/// offsets `pack_slot` reaches for that pair. `None` when `applies_to_layer`
161/// is false for it or the layer is out of range. Called by `rdma_stage` and
162/// tests.
163#[cfg_attr(not(all(feature = "cuda", unix)), allow(dead_code))]
164pub(crate) fn module_slot_offsets(
165 cfg: &ModelConfig,
166 max_rank: usize,
167 target_layer: usize,
168 target_module: LoraModule,
169) -> Option<(usize, usize)> {
170 // 2026-09-25: The same walk as `pack_slot` and `pool_slot_bytes`; RDMA
171 // landing writes at these offsets.
172 let mut off = 0usize;
173 for layer_idx in 0..cfg.num_hidden_layers {
174 for module in LoraModule::ALL {
175 if !module.applies_to_layer(cfg, layer_idx) {
176 continue;
177 }
178 let (out_dim, in_dim) = module.dims(cfg);
179 let a_off = off;
180 let b_off = off + max_rank * in_dim * BF16_BYTES;
181 off = b_off + out_dim * max_rank * BF16_BYTES;
182 if layer_idx == target_layer && module == target_module {
183 return Some((a_off, b_off));
184 }
185 }
186 }
187 None
188}
189
190#[cfg(test)]
191#[path = "slot_math_tests.rs"]
192mod tests;