diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 18:53:33 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 18:53:33 -0300 |
| commit | 78192f5934a2abcbd6a3a256f1d9bb32164cd2cc (patch) | |
| tree | b0be74b7d4eedad6267cfb414bb3068d911ceefa /src/adapter.rs | |
| download | llm-tune-78192f5934a2abcbd6a3a256f1d9bb32164cd2cc.tar.gz llm-tune-78192f5934a2abcbd6a3a256f1d9bb32164cd2cc.zip | |
Diffstat (limited to 'src/adapter.rs')
| -rw-r--r-- | src/adapter.rs | 67 |
1 files changed, 67 insertions, 0 deletions
diff --git a/src/adapter.rs b/src/adapter.rs new file mode 100644 index 0000000..efc94ff --- /dev/null +++ b/src/adapter.rs @@ -0,0 +1,67 @@ +use anyhow::{bail, Result}; +use burn::prelude::*; +use burn::store::{ModuleSnapshot, SafetensorsStore}; +use std::path::Path; + +use crate::config::LoraConfig; +use crate::model::Llama; + +const WEIGHTS: &str = "adapter.safetensors"; +const CONFIG: &str = "adapter_config.json"; + +/// Only the adapter tensors: `....lora.a` and `....lora.b`. +/// Everything else (base weights, norms, embeddings) stays out of the file. +const LORA_RE: &str = r"\.lora\.[ab]$"; + +/// Saves only the adapter. No `ModuleAdapter` is applied: these tensors are +/// created and loaded in burn layout, so transposing them like the HF +/// weights would corrupt them. +pub fn save<B: Backend>(model: &Llama<B>, lora: &LoraConfig, dir: impl AsRef<Path>) -> Result<()> { + let dir = dir.as_ref(); + std::fs::create_dir_all(dir)?; + + let mut store = SafetensorsStore::from_file(dir.join(WEIGHTS)).with_regex(LORA_RE); + model.save_into(&mut store)?; + + std::fs::write(dir.join(CONFIG), serde_json::to_string_pretty(lora)?)?; + Ok(()) +} + +/// Reads the adapter config. It is needed BEFORE building the model: the +/// rank defines the shape of A and B, and the targets which projections get them. +pub fn load_config(dir: impl AsRef<Path>) -> Result<LoraConfig> { + let path = dir.as_ref().join(CONFIG); + if !path.exists() { + bail!("{} does not exist", path.display()); + } + Ok(serde_json::from_str(&std::fs::read_to_string(&path)?)?) +} + +/// Applies an adapter to a model already loaded with the base weights. +pub fn load<B: Backend>(model: &mut Llama<B>, dir: impl AsRef<Path>) -> Result<usize> { + let path = dir.as_ref().join(WEIGHTS); + if !path.exists() { + bail!("{} does not exist", path.display()); + } + + // `allow_partial`: the file only holds LoRA; the rest of the model keeps + // the base weights that were already loaded. + let mut store = SafetensorsStore::from_file(&path).allow_partial(true); + let result = model.load_from(&mut store)?; + + if !result.errors.is_empty() { + bail!("errors applying the adapter: {:?}", result.errors); + } + // `allow_partial` also lets an incomplete file through: tensors that are + // not applied would keep their random init and silently corrupt the + // output. Require all of them. + let expected = model.trainable_ids().len(); + if result.applied.len() != expected { + bail!( + "the adapter applied {} of {} expected tensors; different rank or targets?", + result.applied.len(), + expected + ); + } + Ok(result.applied.len()) +} |