metrale_model_layers/lora/
overlay.rs1use anyhow::{Result, bail};
23
24pub const ROWDIFF_THRESH: f32 = 0.1;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum OverlayModule {
31 EmbedTokens,
34 LmHead,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum OverlayTensorKind {
42 Base,
45 Delta,
48 FullSave,
51 LoraEmbedA,
53 LoraEmbedB,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct OverlayTensor {
60 pub module: OverlayModule,
61 pub kind: OverlayTensorKind,
62}
63
64pub fn classify_overlay_key(key: &str) -> Option<OverlayTensor> {
72 let stripped = key.strip_prefix("base_model.model.")?;
73 if stripped.ends_with(".lora_A.weight") || stripped.ends_with(".lora_B.weight") {
74 return None;
75 }
76 let module = overlay_module_of(stripped)?;
77 let kind = if stripped.ends_with(".token_adapter.base_layer.weight") {
78 OverlayTensorKind::Base
79 } else if stripped.ends_with(".token_adapter.trainable_tokens_delta") {
80 OverlayTensorKind::Delta
81 } else if stripped.ends_with(".lora_embedding_A") {
82 OverlayTensorKind::LoraEmbedA
83 } else if stripped.ends_with(".lora_embedding_B") {
84 OverlayTensorKind::LoraEmbedB
85 } else if is_full_module_weight(stripped, module) {
86 OverlayTensorKind::FullSave
87 } else {
88 return None;
89 };
90 Some(OverlayTensor { module, kind })
91}
92
93fn overlay_module_of(stripped: &str) -> Option<OverlayModule> {
96 if stripped.contains("lm_head") {
97 Some(OverlayModule::LmHead)
98 } else if stripped.contains("embed_tokens") || stripped.contains(".shared.") {
99 Some(OverlayModule::EmbedTokens)
100 } else {
101 None
102 }
103}
104
105fn is_full_module_weight(stripped: &str, module: OverlayModule) -> bool {
108 if stripped.contains(".layers.") {
109 return false;
110 }
111 let leaf_owner = match module {
112 OverlayModule::EmbedTokens => "embed_tokens.weight",
113 OverlayModule::LmHead => "lm_head.weight",
114 };
115 stripped.ends_with(leaf_owner)
116}
117
118#[derive(Debug, Default, Clone)]
121pub struct OverlayTensors {
122 pub embed_base: Option<String>,
123 pub embed_delta: Option<String>,
124 pub embed_full: Option<String>,
125 pub lmhead_base: Option<String>,
126 pub lmhead_delta: Option<String>,
127 pub lmhead_full: Option<String>,
128 pub lora_embedding_seen: bool,
130}
131
132impl OverlayTensors {
133 pub fn insert(&mut self, t: OverlayTensor, name: &str) -> Result<()> {
136 use OverlayModule::*;
137 use OverlayTensorKind::*;
138 let slot = match (t.module, t.kind) {
139 (_, LoraEmbedA) | (_, LoraEmbedB) => {
140 self.lora_embedding_seen = true;
141 return Ok(());
142 }
143 (EmbedTokens, Base) => &mut self.embed_base,
144 (EmbedTokens, Delta) => &mut self.embed_delta,
145 (EmbedTokens, FullSave) => &mut self.embed_full,
146 (LmHead, Base) => &mut self.lmhead_base,
147 (LmHead, Delta) => &mut self.lmhead_delta,
148 (LmHead, FullSave) => &mut self.lmhead_full,
149 };
150 if slot.is_some() {
151 bail!(
152 "REJECT[duplicate-overlay-tensor]: two tensors map to {:?}/{:?}",
153 t.module,
154 t.kind
155 );
156 }
157 *slot = Some(name.to_string());
158 Ok(())
159 }
160
161 pub fn is_empty(&self) -> bool {
163 self.embed_base.is_none()
164 && self.embed_delta.is_none()
165 && self.embed_full.is_none()
166 && self.lmhead_base.is_none()
167 && self.lmhead_delta.is_none()
168 && self.lmhead_full.is_none()
169 && !self.lora_embedding_seen
170 }
171}
172
173pub fn reject_pending_overlay(overlay: &OverlayTensors) -> Result<()> {
176 if overlay.lora_embedding_seen {
177 bail!(
178 "REJECT[lora-embedding-unimplemented]: classic low-rank embedding LoRA \
179 (lora_embedding_A/B) is not yet supported on the decoder path"
180 );
181 }
182 Ok(())
183}
184
185pub fn clamp_trainable_to_vocab(
196 trainable: &[u32],
197 r: usize,
198 vocab: usize,
199) -> Result<(Vec<u32>, usize)> {
200 let mut kept = Vec::new();
201 let mut skipped = 0usize;
202 let mut last_kept: Option<u32> = None;
203 for &idx in trainable {
204 let i = idx as usize;
205 if i >= r {
206 bail!("REJECT[trainable-index-out-of-adapter]: id {idx} >= adapter embedding rows {r}");
207 }
208 if i >= vocab {
209 skipped += 1;
210 continue;
211 }
212 if skipped > 0 {
213 bail!(
214 "REJECT[trainable-order]: served-vocab id {idx} appears after a skipped \
215 vocab-extension id; extension ids must form one trailing suffix"
216 );
217 }
218 if let Some(prev) = last_kept
219 && idx <= prev
220 {
221 bail!(
222 "REJECT[trainable-order]: kept id {idx} follows id {prev}; \
223 PEFT trainable-token order must be strictly ascending in the served-vocab prefix"
224 );
225 }
226 last_kept = Some(idx);
227 kept.push(idx);
228 }
229 Ok((kept, skipped))
230}
231
232pub fn build_override_set(row_diff: &[bool], trainable: &[u32]) -> Vec<u32> {
235 let mut ids: Vec<u32> = row_diff
236 .iter()
237 .enumerate()
238 .filter_map(|(i, &d)| d.then_some(i as u32))
239 .collect();
240 ids.extend_from_slice(trainable);
241 ids.sort_unstable();
242 ids.dedup();
243 ids
244}
245
246pub fn override_source(id: u32, trainable: &[u32]) -> Option<usize> {
250 trainable.iter().position(|&t| t == id)
251}
252
253#[cfg(test)]
254#[path = "overlay_tests.rs"]
255mod tests;