aboutsummaryrefslogtreecommitdiffstats
path: root/src/adapter.rs
blob: efc94ff2221c3f03d6f565b77e147fb7a58e965d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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())
}