/** * lora.ts — Low-Rank Adaptation (LoRA) + QLoRA for the EvermindLM CPU reference. * * The Meta llama-cookbook's central PEFT idea: freeze the base weights and train * a tiny low-rank delta ΔW = (α/r)·B·A on top. Three payoffs, all realised here: * * • Cheap — you train r·(rows+cols) params instead of rows·cols. For the tied * embedding (vocab×dModel) at rank 8 that is orders of magnitude smaller. * • Composable — the adapter serialises to a few KB (see {@link LoRAAdapter.serialize}), * so a persona / tenant / project is an MB-scale artifact you swap at load * time, not a full checkpoint. * • Forgetting-safe — the base never moves, so an adapter cannot catastrophically * overwrite the pretrained model (the exact property WSLA approximates with a * trust region; LoRA gets it structurally). * * QLoRA adds one more: the frozen base is held QUANTIZED (fp16 or int8) and * dequantized on the fly for the merged forward, while the small adapter trains * in fp32. On a single constrained device (our WebGPU target) the frozen base is * where most bytes live, so quantizing it is the biggest memory unlock. * * Pure CPU, exact gradients (finite-difference checked in tests). The WGSL path * is a future acceleration with the same shapes — same contract as EvermindLM. */ import { type AdamWOptions } from "../optim/adamw.js"; /** How the frozen base matrix is stored. `none` = fp32 (plain LoRA); the others = QLoRA. */ export type BaseQuant = "none" | "fp16" | "int8"; export interface LoRAConfig { /** Low-rank bottleneck. Higher = more capacity, larger adapter. Default 8. */ rank?: number; /** LoRA scaling; the delta is scaled by alpha/rank. Default = rank (unit scale). */ alpha?: number; /** Deterministic init seed for the A matrix. */ seed?: number; } /** * A low-rank adapter over one base matrix of shape [rows × cols] (row-major). * * ΔW[i,j] = (α/r) · Σ_k B[i,k]·A[k,j] B: [rows×r] A: [r×cols] * * Standard LoRA init: A ~ N(0, σ²), B = 0, so ΔW starts at exactly zero and the * adapted model equals the base until training moves it. Only A and B are * trainable ({@link parameters}/{@link gradients} expose them to {@link AdamW}); * the base is supplied by the caller and never mutated here. */ export declare class LoRAAdapter { readonly rows: number; readonly cols: number; readonly rank: number; readonly alpha: number; readonly scale: number; /** [rows × rank], zero-initialised. */ readonly B: Float32Array; /** [rank × cols], gaussian-initialised. */ readonly A: Float32Array; private readonly gB; private readonly gA; constructor(rows: number, cols: number, config?: LoRAConfig); /** Trainable count — the LoRA saving vs a full rows·cols matrix. */ numParams(): number; /** Materialise ΔW = scale·B·A as a flat [rows·cols] row-major array. */ delta(): Float32Array; /** Base + ΔW (new array; `base` is not mutated). */ applyTo(base: Float32Array): Float32Array; /** * Project dL/dW (flat [rows·cols], the gradient the base matrix WOULD receive) * onto the adapter, accumulating dL/dA and dL/dB. Exact chain rule for * ΔW = scale·B·A: gA = scale·Bᵀ·G, gB = scale·G·Aᵀ. */ accumulateGradient(gW: Float32Array): void; parameters(): { data: Float32Array; }[]; gradients(): { data: Float32Array; }[]; zeroGrad(): void; /** Compact self-describing adapter blob (magic, rows, cols, rank, alpha, B, A). */ serialize(): ArrayBuffer; static deserialize(buffer: ArrayBuffer): LoRAAdapter; } /** Quantize a base matrix for QLoRA storage; returns a dequantized view + byte cost. */ export declare function quantizeBase(base: Float32Array, mode: BaseQuant): { view: Float32Array; bytes: number; }; import { EvermindLM } from "../lm/evermind_lm.js"; export interface LoRAFitOptions extends AdamWOptions { epochs?: number; /** * Gradient accumulation: average the adapter gradient over this many sequences * (micro-batches) before each optimiser step. Lets a memory-constrained device * train at a larger *effective* batch. Default 1 (step per sequence). */ accumSteps?: number; } /** * LoRA / QLoRA fine-tuning of an {@link EvermindLM} through its tied token * embedding — the dominant parameter (vocab×dModel) and the natural adapter * target (input lookup and output head share it). The base model is frozen; only * the {@link LoRAAdapter} trains. * * QLoRA: pass `baseQuant: "fp16" | "int8"` and the frozen base is stored * quantized and dequantized on the fly for the merged forward, so the resident * base costs half (fp16) or a quarter (int8) of the bytes while the adapter * trains full-precision. * * The adapter is the shippable artifact: {@link serializeAdapter} emits a few KB * you swap per persona/tenant/project, versus rewriting the whole checkpoint. */ export declare class EvermindLMLoRA { private readonly model; readonly adapter: LoRAAdapter; readonly baseQuant: BaseQuant; /** Frozen base embedding as used in the merged forward (dequantized under QLoRA). */ private readonly frozenBase; private readonly baseBytesStored; private readonly rows; private readonly cols; constructor(model: EvermindLM, config?: LoRAConfig & { baseQuant?: BaseQuant; }); /** The frozen base model this adapter rides on. */ get baseModel(): EvermindLM; /** Effective embedding used for training/generation: frozenBase + adapter delta. */ mergedEmb(): Float32Array; /** * One forward+backward on the merged weights, projecting the base-embedding * gradient onto the adapter. The underlying model's own weights are left * exactly as they were (base frozen); adapter gradients ACCUMULATE (caller * zeroes between optimiser windows). */ private _accumulate; /** Train the adapter (only) with AdamW + optional gradient accumulation. */ fit(sequences: number[][], opts?: LoRAFitOptions): number[]; generate(prompt: number[], opts: import("../lm/evermind_lm.js").LMGenerateOptions): number[]; generateText(prompt: string, codec: import("../lm/evermind_lm.js").TextCodec, opts: import("../lm/evermind_lm.js").LMGenerateOptions): string; /** The shippable adapter artifact (a few KB). */ serializeAdapter(): ArrayBuffer; /** Reconstruct a fine-tuned model from a base model + a serialized adapter. */ static loadAdapter(model: EvermindLM, adapterBuffer: ArrayBuffer, baseQuant?: BaseQuant): EvermindLMLoRA; /** Bake the adapter into the base and return a standalone EVL0 checkpoint. */ merge(opts?: { fp16?: boolean; }): ArrayBuffer; /** Byte cost of the trainable adapter vs the frozen base — the LoRA/QLoRA saving. */ footprint(): { adapterBytes: number; baseBytes: number; trainableParams: number; baseParams: number; }; } //# sourceMappingURL=lora.d.ts.map