std1.0.0-beta-7 SafeC docs

Source ml/lora.h

View in the repository

#pragma once
// SafeC Standard Library — parameter-efficient fine-tuning: LoRA and QLoRA.
//
// LoRA (Hu et al. 2021) freezes a pretrained network and learns a low-rank
// update for each linear layer:
//   y = x W + b + (alpha / r) * (dropout(x) A) B
// with A [in, r] random (Kaiming-uniform) and B [r, out] zero, so the
// adapted network starts exactly equal to the base. Only A and B train:
// r * (in + out) values per layer instead of in * out. LoraMlp::merge folds
// the update into W for deployment (no extra cost at inference).
//
// QLoRA (Dettmers et al. 2023) does the same on top of a base whose
// weights are stored in 4-bit NormalFloat (NF4): each block of 64 weights
// keeps one absmax scale and a 4-bit index into the 16 NF4 levels (the
// quantiles of a standard normal, so normally distributed weights use
// every level evenly). Double quantization also stores those scales in 8
// bits. Weights are dequantized to float32 per forward pass, so the
// frozen base costs ~4.1-4.5 bits per weight at rest while gradients flow
// in float32 through it to the adapters.
//
// Both wrap an Mlp (nn.h) and are Modules (module.h), so they train like
// any other model:
//   struct LoraMlp ft = LoraMlp::attach(&pretrained, &cfg);
//   model_add_params(&ft, &opt);       // adapters only
//   module_fit(&ft, TRAIN_LOSS_CE, &opt, &data, &tc);
#include <std/ml/tensor.h>
#include <std/ml/nn.h>
#include <std/collections/vec.h>

namespace std {

struct LoraConfig {
    unsigned long rank;   // r
    float alpha;          // the update is scaled by alpha / r
    float dropout;        // on the adapter's input while training (grad enabled)
    unsigned long seed;

    // alpha = rank (scale 1), no dropout.
    static struct LoraConfig default(unsigned long rank);
};

// ── LoRA ─────────────────────────────────────────────────────────────────────
// Borrows the base Mlp and freezes its parameters (requiresGrad = 0) until
// LoraMlp::detach; the base must outlive the adapter.
struct LoraMlp {
    &Mlp base;
    struct Vec<&FloatTensor> A;    // per layer [in, r]
    struct Vec<&FloatTensor> B;    // per layer [r, out]
    struct LoraConfig cfg;
    float scale;              // alpha / r
    struct Rng rng;

    &FloatTensor forward(const &FloatTensor x);
    // Parameterized (module.h): the adapters (A_0, B_0, A_1, B_1, ...), which
    // are all that trains and all a LoRA file holds.
    void collect_params(&Vec<&FloatTensor> out) const;
    void collect_state(&Vec<&FloatTensor> out) const;
    // W += scale * A B in the base for every layer, then B = 0: the base alone
    // now computes what the adapted network did.
    void merge();
    // Frees the adapters and unfreezes the base.
    void detach();

    static struct LoraMlp attach(&Mlp base, const &LoraConfig cfg);
};

// ── NF4 quantization ─────────────────────────────────────────────────────────
#define NF4_BLOCK        64UL    // weights per absmax scale
#define NF4_SCALE_BLOCK  256UL   // scales per second-level scale (double quantization)

struct Nf4Tensor {
    &heap unsigned char codes;      // two 4-bit level indices per byte, low nibble first
    &heap float absmax;             // one per block (when not double-quantized)
    &heap unsigned char absmaxQ;    // int8 + 128 per block (double-quantized)
    &heap float absmaxScale;        // one per NF4_SCALE_BLOCK blocks
    float absmaxMean;               // subtracted before the 8-bit quantization
    unsigned long rows;
    unsigned long cols;
    unsigned long blockSize;
    unsigned long blocks;
    int doubleQuant;

    unsigned long bytes() const;   // storage, all parts
    void drop();
    void free() consume;   // = drop()

    static struct Nf4Tensor quantize(const &FloatTensor w, unsigned long blockSize, int doubleQuant);
    &FloatTensor dequantize() const;   // [rows, cols], no grad
};

// ── QLoRA ────────────────────────────────────────────────────────────────────
// Copies the base: weights to NF4, biases as float32, all frozen. The base
// Mlp may be freed afterwards.
struct QLoraMlp {
    struct Vec<struct Nf4Tensor> W;
    struct Vec<&FloatTensor> bias;
    struct Vec<&FloatTensor> A;
    struct Vec<&FloatTensor> B;
    unsigned long nLayers;
    int act;
    int outAct;
    struct LoraConfig cfg;
    float scale;
    struct Rng rng;

    &FloatTensor forward(const &FloatTensor x);
    // Parameterized (module.h): the adapters, as for LoraMlp.
    void collect_params(&Vec<&FloatTensor> out) const;
    void collect_state(&Vec<&FloatTensor> out) const;
    unsigned long base_bytes() const;        // the frozen base at rest
    void drop();
    void free() consume;   // = drop()

    static struct QLoraMlp attach(const &Mlp base, const &LoraConfig cfg, unsigned long blockSize, int doubleQuant);
};

} // namespace std