Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- README.md +21 -5
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/baselines.cpython-313.pyc +0 -0
- app.py +358 -0
- baselines.py +98 -0
- requirements.txt +5 -0
README.md
CHANGED
|
@@ -1,14 +1,30 @@
|
|
| 1 |
---
|
| 2 |
title: TensorPEFT
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.31.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: TensorPEFT
|
| 3 |
+
emoji: 🧊
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.31.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
+
short_description: Tensor-pursuit fine-tuning vs LoRA at tiny budgets
|
| 12 |
+
tags:
|
| 13 |
+
- peft
|
| 14 |
+
- fine-tuning
|
| 15 |
+
- tensor-decomposition
|
| 16 |
+
- lora-alternative
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# TensorPEFT live demo
|
| 20 |
+
|
| 21 |
+
Live race between **TensorPEFT** (tensor gradient-pursuit adapters) and matched-budget PEFT baselines
|
| 22 |
+
(prompt tuning, LoRA, (IA)³, LayerNorm tuning): GPT-2 124M is fine-tuned on Bengali news with the same number of
|
| 23 |
+
trainable parameters for every method, and validation cross-entropy decides.
|
| 24 |
+
|
| 25 |
+
- Library: [Angshul/TensorPEFT](https://huggingface.co/Angshul/TensorPEFT) — `pip install git+https://huggingface.co/Angshul/TensorPEFT`
|
| 26 |
+
- Data: Bengali news articles from [soham96/Bengali_news_classifier](https://github.com/soham96/Bengali_news_classifier),
|
| 27 |
+
downloaded at start-up (not redistributed), tokenised with the GPT-2 tokenizer; 3M training tokens, held-out articles for validation.
|
| 28 |
+
- Base model: [openai-community/gpt2](https://huggingface.co/openai-community/gpt2).
|
| 29 |
+
|
| 30 |
+
Runs on ZeroGPU. Benchmark numbers in the *Benchmark results* tab were measured on CPU (fp32, 300 steps).
|
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (28.9 kB). View file
|
|
|
__pycache__/baselines.cpython-313.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TensorPEFT live demo: tensor gradient-pursuit adapters vs. matched-budget PEFT baselines.
|
| 2 |
+
|
| 3 |
+
GPT-2 (124M, English) is fine-tuned on Bengali news with the same number of trainable
|
| 4 |
+
parameters for every method; validation cross-entropy (nats/token) decides.
|
| 5 |
+
"""
|
| 6 |
+
import copy
|
| 7 |
+
import glob
|
| 8 |
+
import io
|
| 9 |
+
import json
|
| 10 |
+
import math
|
| 11 |
+
import os
|
| 12 |
+
import random
|
| 13 |
+
import re
|
| 14 |
+
import time
|
| 15 |
+
import unicodedata
|
| 16 |
+
import urllib.request
|
| 17 |
+
import zipfile
|
| 18 |
+
|
| 19 |
+
import gradio as gr
|
| 20 |
+
import matplotlib
|
| 21 |
+
|
| 22 |
+
matplotlib.use("Agg")
|
| 23 |
+
import matplotlib.pyplot as plt # noqa: E402
|
| 24 |
+
import numpy as np # noqa: E402
|
| 25 |
+
import torch # noqa: E402
|
| 26 |
+
import torch.nn.functional as F # noqa: E402
|
| 27 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer # noqa: E402
|
| 28 |
+
|
| 29 |
+
from baselines import IA3, LNTune, LoRA, Prompt, _Adapter # noqa: E402
|
| 30 |
+
from tensorpeft import TensorPEFTConfig, get_tensorpeft_model # noqa: E402
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import spaces
|
| 34 |
+
GPU = spaces.GPU
|
| 35 |
+
except Exception: # running outside a ZeroGPU Space
|
| 36 |
+
def GPU(fn=None, **kw):
|
| 37 |
+
return fn if fn is not None else (lambda f: f)
|
| 38 |
+
|
| 39 |
+
MODEL_ID = os.environ.get("TP_MODEL", "openai-community/gpt2")
|
| 40 |
+
DATA_URL = os.environ.get(
|
| 41 |
+
"TP_DATA_URL",
|
| 42 |
+
"https://raw.githubusercontent.com/soham96/Bengali_news_classifier/"
|
| 43 |
+
"4c2a51b51fc56208426ef19368bfcb08acc9d0c4/data/data.zip")
|
| 44 |
+
CACHE = os.environ.get("TP_CACHE", "data_cache")
|
| 45 |
+
SEQ, BATCH, VAL_SEQS = 128, 8, 64
|
| 46 |
+
|
| 47 |
+
# ------------------------------------------------------------------ data (built once at start-up)
|
| 48 |
+
tok = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def build_data():
|
| 52 |
+
os.makedirs(CACHE, exist_ok=True)
|
| 53 |
+
tr, va = os.path.join(CACHE, "train.bin"), os.path.join(CACHE, "val.bin")
|
| 54 |
+
if os.path.exists(tr) and os.path.exists(va):
|
| 55 |
+
return
|
| 56 |
+
src = DATA_URL
|
| 57 |
+
raw = open(src, "rb").read() if os.path.exists(src) else urllib.request.urlopen(src).read()
|
| 58 |
+
docs = []
|
| 59 |
+
with zipfile.ZipFile(io.BytesIO(raw)) as z:
|
| 60 |
+
for name in sorted(n for n in z.namelist() if n.endswith("_articles.txt") and "__MACOSX" not in n):
|
| 61 |
+
for a in json.loads(z.read(name).decode("utf-8"))["articles"]:
|
| 62 |
+
t = unicodedata.normalize("NFC", (a.get("title", "") + "। " + a.get("body", "")).strip())
|
| 63 |
+
t = re.sub(r"\s+", " ", t)
|
| 64 |
+
if len(t) > 200:
|
| 65 |
+
docs.append(t)
|
| 66 |
+
random.Random(0).shuffle(docs)
|
| 67 |
+
|
| 68 |
+
def enc(ds, cap):
|
| 69 |
+
ids = []
|
| 70 |
+
for d in ds:
|
| 71 |
+
ids += tok(d).input_ids + [tok.eos_token_id]
|
| 72 |
+
if len(ids) >= cap:
|
| 73 |
+
break
|
| 74 |
+
return np.array(ids[:cap], dtype=np.uint16)
|
| 75 |
+
|
| 76 |
+
enc(docs[:400], 200_000).tofile(va)
|
| 77 |
+
enc(docs[400:], 3_000_000).tofile(tr)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
build_data()
|
| 81 |
+
TRAIN = np.fromfile(os.path.join(CACHE, "train.bin"), dtype=np.uint16)
|
| 82 |
+
VAL = torch.from_numpy(np.fromfile(os.path.join(CACHE, "val.bin"), dtype=np.uint16)[: VAL_SEQS * (SEQ + 1)]
|
| 83 |
+
.astype(np.int64).reshape(VAL_SEQS, SEQ + 1))
|
| 84 |
+
BASE = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.float32).eval()
|
| 85 |
+
for _p in BASE.parameters():
|
| 86 |
+
_p.requires_grad_(False)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def sampler(seed):
|
| 90 |
+
rng = np.random.default_rng(seed)
|
| 91 |
+
while True:
|
| 92 |
+
ix = rng.integers(0, len(TRAIN) - SEQ - 1, BATCH)
|
| 93 |
+
yield torch.from_numpy(np.stack([TRAIN[i:i + SEQ + 1] for i in ix]).astype(np.int64))
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# ------------------------------------------------------------------ methods per budget
|
| 97 |
+
class TP(_Adapter):
|
| 98 |
+
lr, warmup = 3e-3, 10
|
| 99 |
+
|
| 100 |
+
def __init__(self, model, **cfg):
|
| 101 |
+
super().__init__()
|
| 102 |
+
self.model = [model]
|
| 103 |
+
self.tp = get_tensorpeft_model(model, TensorPEFTConfig(**cfg))
|
| 104 |
+
|
| 105 |
+
def setup(self, probe_batches):
|
| 106 |
+
self.tp.init_atoms(probe_batches, verbose=False)
|
| 107 |
+
|
| 108 |
+
def trainable(self):
|
| 109 |
+
return [p for p in self.tp.parameters() if p.requires_grad]
|
| 110 |
+
|
| 111 |
+
def mark_ready(self):
|
| 112 |
+
for g in self.tp.groups:
|
| 113 |
+
g.initialized.fill_(True)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
ATTN = ["c_attn.q", "c_attn.k", "c_attn.v", "attn.c_proj"]
|
| 117 |
+
BUDGETS = {
|
| 118 |
+
"≈1.5k parameters": [
|
| 119 |
+
("TensorPEFT · 1 attention atom", lambda m: TP(m, target_modules=ATTN, atoms_per_group=1)),
|
| 120 |
+
("Prompt tuning · 1 token", lambda m: Prompt(m, 1)),
|
| 121 |
+
("LayerNorm tuning · final LN", lambda m: LNTune(m, layers=[], final=True)),
|
| 122 |
+
],
|
| 123 |
+
"≈9k parameters": [
|
| 124 |
+
("TensorPEFT · 3 atoms", lambda m: TP(m, atoms_per_group=1)),
|
| 125 |
+
("Prompt tuning · 12 tokens", lambda m: Prompt(m, 12)),
|
| 126 |
+
("LoRA r=1 · q,v of last 3 layers", lambda m: LoRA(m, 1, layers=[9, 10, 11])),
|
| 127 |
+
("(IA)³ · values", lambda m: IA3(m, kv=("c_attn.v",), ff=False)),
|
| 128 |
+
("LayerNorm tuning · last 3 layers", lambda m: LNTune(m, layers=[9, 10, 11], final=False)),
|
| 129 |
+
],
|
| 130 |
+
"≈55k parameters": [
|
| 131 |
+
("TensorPEFT · 18 atoms", lambda m: TP(m, atoms_per_group=6)),
|
| 132 |
+
("(IA)³ · keys, values, MLP", lambda m: IA3(m)),
|
| 133 |
+
],
|
| 134 |
+
}
|
| 135 |
+
COLORS = ["#7c3aed", "#0ea5e9", "#f59e0b", "#10b981", "#ef4444"]
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def ce(adapter, x):
|
| 139 |
+
return F.cross_entropy(adapter.logits(x[:, :-1]).flatten(0, 1), x[:, 1:].flatten())
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@torch.no_grad()
|
| 143 |
+
def evaluate(adapter, dev):
|
| 144 |
+
V = VAL.to(dev)
|
| 145 |
+
return sum(float(ce(adapter, V[i:i + 16])) * len(V[i:i + 16]) for i in range(0, len(V), 16)) / len(V)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _duration(budget, steps, seed):
|
| 149 |
+
return int(min(280, 30 + len(BUDGETS[budget]) * steps * 0.06))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@GPU(duration=_duration)
|
| 153 |
+
def race_gpu(budget, steps, seed):
|
| 154 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 155 |
+
if dev == "cuda":
|
| 156 |
+
torch.backends.cuda.matmul.allow_tf32 = False
|
| 157 |
+
torch.backends.cudnn.allow_tf32 = False
|
| 158 |
+
out = []
|
| 159 |
+
plain = _Adapter()
|
| 160 |
+
plain.model = [copy.deepcopy(BASE).to(dev)]
|
| 161 |
+
base_val = evaluate(plain, dev)
|
| 162 |
+
del plain
|
| 163 |
+
for mi, (name, build) in enumerate(BUDGETS[budget]):
|
| 164 |
+
torch.manual_seed(seed)
|
| 165 |
+
model = copy.deepcopy(BASE).to(dev).eval()
|
| 166 |
+
ad = build(model)
|
| 167 |
+
t0 = time.time()
|
| 168 |
+
if isinstance(ad, TP):
|
| 169 |
+
ad.setup([b.to(dev) for b, _ in zip(sampler(seed + 12345), range(4))])
|
| 170 |
+
params = ad.trainable()
|
| 171 |
+
opt = torch.optim.AdamW(params, lr=ad.lr, weight_decay=0.0)
|
| 172 |
+
warm = getattr(ad, "warmup", 0)
|
| 173 |
+
batches = sampler(seed)
|
| 174 |
+
curve = [(0, evaluate(ad, dev))]
|
| 175 |
+
for step in range(1, steps + 1):
|
| 176 |
+
if warm:
|
| 177 |
+
for g in opt.param_groups:
|
| 178 |
+
g["lr"] = ad.lr * min(1.0, step / warm)
|
| 179 |
+
loss = ce(ad, next(batches).to(dev))
|
| 180 |
+
opt.zero_grad(set_to_none=True)
|
| 181 |
+
loss.backward()
|
| 182 |
+
opt.step()
|
| 183 |
+
if step % 25 == 0 or step == steps:
|
| 184 |
+
curve.append((step, evaluate(ad, dev)))
|
| 185 |
+
out.append(dict(name=name, n_params=ad.n_params(), curve=curve, final=curve[-1][1],
|
| 186 |
+
seconds=time.time() - t0,
|
| 187 |
+
state=[p.detach().float().cpu().clone() for p in params]))
|
| 188 |
+
del model, ad, opt
|
| 189 |
+
return dict(budget=budget, steps=steps, seed=seed, base=base_val, runs=out)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def run_race(budget, steps, seed):
|
| 193 |
+
res = race_gpu(budget, int(steps), int(seed))
|
| 194 |
+
fig, ax = plt.subplots(figsize=(8, 4.6))
|
| 195 |
+
for i, r in enumerate(res["runs"]):
|
| 196 |
+
xs, ys = zip(*r["curve"])
|
| 197 |
+
ax.plot(xs, ys, color=COLORS[i % len(COLORS)], lw=2.6 if r["name"].startswith("TensorPEFT") else 1.6,
|
| 198 |
+
label=f"{r['name']} ({r['n_params']:,} params)")
|
| 199 |
+
ax.axhline(res["base"], color="#9ca3af", ls="--", lw=1, label=f"GPT-2 before fine-tuning ({res['base']:.3f})")
|
| 200 |
+
ax.set_xlabel("training step")
|
| 201 |
+
ax.set_ylabel("validation loss (nats/token) ↓ better")
|
| 202 |
+
ax.set_title(f"Bengali fine-tuning of GPT-2 at {res['budget']}")
|
| 203 |
+
ax.grid(alpha=0.25)
|
| 204 |
+
ax.legend(fontsize=8, loc="upper right")
|
| 205 |
+
fig.tight_layout()
|
| 206 |
+
ranked = sorted(res["runs"], key=lambda r: r["final"])
|
| 207 |
+
best = ranked[0]
|
| 208 |
+
rows = [[i + 1, r["name"], f"{r['n_params']:,}", f"{r['final']:.4f}", f"{r['final'] - best['final']:+.4f}",
|
| 209 |
+
f"{r['seconds']:.0f}s"] for i, r in enumerate(ranked)]
|
| 210 |
+
tp = next(r for r in res["runs"] if r["name"].startswith("TensorPEFT"))
|
| 211 |
+
others = [r for r in res["runs"] if r is not tp]
|
| 212 |
+
nxt = min(others, key=lambda r: r["final"])
|
| 213 |
+
verdict = (f"### {'🏆 TensorPEFT wins' if tp is best else 'TensorPEFT does not win this run'}\n"
|
| 214 |
+
f"TensorPEFT **{tp['final']:.4f}** vs best baseline ({nxt['name']}) **{nxt['final']:.4f}** "
|
| 215 |
+
f"→ difference **{tp['final'] - nxt['final']:+.4f}** nats/token after {res['steps']} steps (seed {res['seed']}).")
|
| 216 |
+
store = {"budget": res["budget"], "runs": [{"name": r["name"], "state": r["state"]} for r in res["runs"]]}
|
| 217 |
+
return fig, rows, verdict, store
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
@GPU(duration=90)
|
| 221 |
+
def generate_gpu(store, prompt, n_new, temperature, seed):
|
| 222 |
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
| 223 |
+
ids0 = tok(prompt, return_tensors="pt").input_ids.to(dev)
|
| 224 |
+
builders = dict(BUDGETS[store["budget"]]) if store else {}
|
| 225 |
+
entries = [("GPT-2 before fine-tuning", None, None)] + \
|
| 226 |
+
[(r["name"], builders[r["name"]], r["state"]) for r in (store["runs"] if store else [])]
|
| 227 |
+
outs = []
|
| 228 |
+
for name, build, state in entries:
|
| 229 |
+
model = copy.deepcopy(BASE).to(dev).eval()
|
| 230 |
+
if build is None:
|
| 231 |
+
ad = _Adapter()
|
| 232 |
+
ad.model = [model]
|
| 233 |
+
else:
|
| 234 |
+
ad = build(model)
|
| 235 |
+
if isinstance(ad, TP):
|
| 236 |
+
ad.mark_ready()
|
| 237 |
+
with torch.no_grad():
|
| 238 |
+
for p, s in zip(ad.trainable(), state):
|
| 239 |
+
p.copy_(s.to(p.device, p.dtype))
|
| 240 |
+
g = torch.Generator(device=dev).manual_seed(int(seed))
|
| 241 |
+
ids = ids0.clone()
|
| 242 |
+
with torch.no_grad():
|
| 243 |
+
for _ in range(int(n_new)):
|
| 244 |
+
logits = ad.logits(ids[:, -512:])[:, -1].float() / max(float(temperature), 1e-3)
|
| 245 |
+
top = torch.topk(logits, 50)
|
| 246 |
+
probs = torch.softmax(top.values, -1)
|
| 247 |
+
nxt = top.indices.gather(-1, torch.multinomial(probs, 1, generator=g))
|
| 248 |
+
ids = torch.cat([ids, nxt], dim=1)
|
| 249 |
+
outs.append((name, tok.decode(ids[0, ids0.shape[1]:], skip_special_tokens=True)))
|
| 250 |
+
del model, ad
|
| 251 |
+
return outs
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def run_generate(store, prompt, n_new, temperature, seed):
|
| 255 |
+
if not prompt.strip():
|
| 256 |
+
return "Enter a Bengali prompt."
|
| 257 |
+
outs = generate_gpu(store, prompt, n_new, temperature, seed)
|
| 258 |
+
note = "" if store else "*Run a race first to compare the fine-tuned models; showing the base model only.*\n\n"
|
| 259 |
+
return note + "\n\n".join(f"**{n}**\n\n> {prompt}{t}" for n, t in outs)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# ------------------------------------------------------------------ benchmark results (CPU, 300 steps)
|
| 263 |
+
RESULTS_MD = """
|
| 264 |
+
### Three seeds (mean ± std of validation loss; lower is better)
|
| 265 |
+
|
| 266 |
+
| budget | TensorPEFT | best baseline | difference | seeds won | p (paired t) |
|
| 267 |
+
|---|---:|---|---:|:---:|---:|
|
| 268 |
+
| ≈9k | **1.3245 ± 0.004** | prompt tuning 1.3684 ± 0.010 | **−0.044** | 3/3 | 0.008 |
|
| 269 |
+
| ≈37k | 1.2558 ± 0.010 | LayerNorm tuning (38.4k) 1.2504 ± 0.003 | +0.005 | 1/3 | 0.37 (tie) |
|
| 270 |
+
| ≈55k | **1.2368 ± 0.017** | (IA)³ 1.2880 ± 0.003 | **−0.051** | 3/3 | 0.026 |
|
| 271 |
+
|
| 272 |
+
### Every method at every budget (seed 0)
|
| 273 |
+
|
| 274 |
+
| budget | TensorPEFT | LoRA | QLoRA | VeRA | (IA)³ | prompt tuning | LayerNorm tuning | BitFit |
|
| 275 |
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
| 276 |
+
| ≈1.5k | **1.461** | – | – | – | – | 1.493 (768) | 1.600 | – |
|
| 277 |
+
| ≈9k | **1.327** | 1.430 | – | – | 1.432 | 1.370 | 1.407 | – |
|
| 278 |
+
| ≈37k | 1.255 | 1.318 | 1.335 | 1.393 | – | 1.327 | **1.253** | – |
|
| 279 |
+
| ≈55k | **1.231** | – | – | – | 1.289 | – | – | – |
|
| 280 |
+
| ≈83k | – | – | – | – | – | – | – | 1.326 |
|
| 281 |
+
|
| 282 |
+
GPT-2 124M before fine-tuning: 1.708. 300 AdamW steps, batch 8 × 128 tokens, identical data for all methods,
|
| 283 |
+
baseline learning rates from their papers. “–”: the method cannot be configured at that budget on GPT-2 or was
|
| 284 |
+
not run there (LoRA's smallest all-layer setting is 37k). At larger budgets (≥ 115k parameters) LoRA was
|
| 285 |
+
better in our tests — TensorPEFT is a method for very small budgets.
|
| 286 |
+
"""
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def results_figure():
|
| 290 |
+
pts = {
|
| 291 |
+
"TensorPEFT": [(1553, 1.461), (9259, 1.327), (37036, 1.255), (55554, 1.231)],
|
| 292 |
+
"LoRA / QLoRA": [(9216, 1.430), (36864, 1.318), (36864, 1.335)],
|
| 293 |
+
"Prompt tuning": [(768, 1.493), (9216, 1.370), (36864, 1.327)],
|
| 294 |
+
"LayerNorm tuning": [(1536, 1.600), (9216, 1.407), (38400, 1.253)],
|
| 295 |
+
"(IA)³": [(9216, 1.432), (55296, 1.289)],
|
| 296 |
+
"VeRA": [(24576, 1.393)],
|
| 297 |
+
"BitFit": [(82944, 1.326)],
|
| 298 |
+
}
|
| 299 |
+
fig, ax = plt.subplots(figsize=(8, 4.6))
|
| 300 |
+
for i, (k, v) in enumerate(pts.items()):
|
| 301 |
+
xs, ys = zip(*v)
|
| 302 |
+
if k == "TensorPEFT":
|
| 303 |
+
ax.plot(xs, ys, "-o", color=COLORS[0], lw=2.6, ms=7, label=k, zorder=5)
|
| 304 |
+
else:
|
| 305 |
+
ax.scatter(xs, ys, s=45, color=["#0ea5e9", "#f59e0b", "#10b981", "#ef4444", "#64748b", "#a16207"][i - 1],
|
| 306 |
+
label=k)
|
| 307 |
+
ax.set_xscale("log")
|
| 308 |
+
ax.set_xlabel("trainable parameters (log scale)")
|
| 309 |
+
ax.set_ylabel("validation loss (nats/token) ↓ better")
|
| 310 |
+
ax.set_title("GPT-2 → Bengali: loss vs. trainable parameters (seed 0)")
|
| 311 |
+
ax.grid(alpha=0.25, which="both")
|
| 312 |
+
ax.legend(fontsize=8)
|
| 313 |
+
fig.tight_layout()
|
| 314 |
+
return fig
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ------------------------------------------------------------------ UI
|
| 318 |
+
INTRO = """
|
| 319 |
+
# TensorPEFT · tensor gradient-pursuit fine-tuning
|
| 320 |
+
LoRA gives every weight matrix its own low-rank update. **TensorPEFT** stacks all equal-shaped matrices of the
|
| 321 |
+
network into one tensor (layers × {q,k,v,o} × d_in × d_out) and learns a few **rank-one CP atoms** of it, chosen from
|
| 322 |
+
one gradient probe before training. Each atom touches every layer for only L + P + d_in + d_out + 1 parameters.
|
| 323 |
+
|
| 324 |
+
Here GPT-2 (English) is taught **Bengali** with the *same number of trainable parameters* for every method.
|
| 325 |
+
`pip install git+https://huggingface.co/Angshul/TensorPEFT`
|
| 326 |
+
"""
|
| 327 |
+
|
| 328 |
+
with gr.Blocks(title="TensorPEFT") as demo:
|
| 329 |
+
gr.Markdown(INTRO)
|
| 330 |
+
store = gr.State(None)
|
| 331 |
+
with gr.Tab("Live race"):
|
| 332 |
+
with gr.Row():
|
| 333 |
+
budget = gr.Radio(list(BUDGETS), value="≈9k parameters", label="Trainable-parameter budget")
|
| 334 |
+
steps = gr.Number(value=300, visible=False)
|
| 335 |
+
seed = gr.Number(value=0, precision=0, label="Seed")
|
| 336 |
+
gr.Markdown("Every method trains for **300 AdamW steps** (batch 8 × 128 tokens) on identical batches — the setting of the benchmark. "
|
| 337 |
+
"TensorPEFT starts from zero coefficients, so it trails in the first few dozen steps and overtakes later.")
|
| 338 |
+
go = gr.Button("Run the race", variant="primary")
|
| 339 |
+
verdict = gr.Markdown()
|
| 340 |
+
plot = gr.Plot()
|
| 341 |
+
table = gr.Dataframe(headers=["rank", "method", "params", "val loss", "vs best", "time"], interactive=False)
|
| 342 |
+
go.click(run_race, [budget, steps, seed], [plot, table, verdict, store])
|
| 343 |
+
with gr.Tab("Generate"):
|
| 344 |
+
gr.Markdown("Continuations from the models trained in your last race (top-k 50 sampling).")
|
| 345 |
+
prompt = gr.Textbox(value="কলকাতা শহরে আজ", label="Bengali prompt")
|
| 346 |
+
with gr.Row():
|
| 347 |
+
n_new = gr.Slider(20, 120, value=60, step=10, label="New tokens")
|
| 348 |
+
temp = gr.Slider(0.3, 1.2, value=0.8, step=0.1, label="Temperature")
|
| 349 |
+
gseed = gr.Number(value=0, precision=0, label="Seed")
|
| 350 |
+
gen = gr.Button("Generate", variant="primary")
|
| 351 |
+
gen_out = gr.Markdown()
|
| 352 |
+
gen.click(run_generate, [store, prompt, n_new, temp, gseed], gen_out)
|
| 353 |
+
with gr.Tab("Benchmark results"):
|
| 354 |
+
gr.Plot(value=results_figure())
|
| 355 |
+
gr.Markdown(RESULTS_MD)
|
| 356 |
+
|
| 357 |
+
if __name__ == "__main__":
|
| 358 |
+
demo.launch()
|
baselines.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Matched-budget PEFT baselines for the demo (GPT-2), built on tensorpeft's slot discovery."""
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
from tensorpeft.layers import attach_hooks, find_slots
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class _Adapter(nn.Module):
|
| 10 |
+
lr = 1e-3
|
| 11 |
+
|
| 12 |
+
def trainable(self):
|
| 13 |
+
return [p for p in self.parameters() if p.requires_grad]
|
| 14 |
+
|
| 15 |
+
def n_params(self):
|
| 16 |
+
return sum(p.numel() for p in self.trainable())
|
| 17 |
+
|
| 18 |
+
def logits(self, ids):
|
| 19 |
+
return self.model[0](ids).logits
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class LoRA(_Adapter):
|
| 23 |
+
lr = 1e-3
|
| 24 |
+
|
| 25 |
+
def __init__(self, model, rank=1, targets=("c_attn.q", "c_attn.v"), layers=None, seed=0):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.model = [model]
|
| 28 |
+
self.slots = [s for s in find_slots(model, list(targets)) if layers is None or s.layer in layers]
|
| 29 |
+
g = torch.Generator().manual_seed(seed)
|
| 30 |
+
dev = next(model.parameters()).device
|
| 31 |
+
self.A = nn.ParameterList([nn.Parameter(((torch.rand(s.shape[0], rank, generator=g) * 2 - 1)
|
| 32 |
+
/ math.sqrt(s.shape[0])).to(dev)) for s in self.slots])
|
| 33 |
+
self.B = nn.ParameterList([nn.Parameter(torch.zeros(rank, s.shape[1], device=dev)) for s in self.slots])
|
| 34 |
+
self.scale = 2.0
|
| 35 |
+
self._i = {id(s): i for i, s in enumerate(self.slots)}
|
| 36 |
+
self._h = attach_hooks(self.slots, lambda s, x: (x @ self.A[self._i[id(s)]]) @ self.B[self._i[id(s)]] * self.scale)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class IA3(_Adapter):
|
| 40 |
+
lr = 3e-3
|
| 41 |
+
|
| 42 |
+
def __init__(self, model, kv=("c_attn.k", "c_attn.v"), ff=True):
|
| 43 |
+
super().__init__()
|
| 44 |
+
self.model = [model]
|
| 45 |
+
self.out_slots = find_slots(model, list(kv))
|
| 46 |
+
self.ff_slots = find_slots(model, ["mlp.c_proj"]) if ff else []
|
| 47 |
+
dev = next(model.parameters()).device
|
| 48 |
+
self.l_out = nn.ParameterList([nn.Parameter(torch.ones(s.shape[1], device=dev)) for s in self.out_slots])
|
| 49 |
+
self.l_ff = nn.ParameterList([nn.Parameter(torch.ones(s.shape[0], device=dev)) for s in self.ff_slots])
|
| 50 |
+
io = {id(s): i for i, s in enumerate(self.out_slots)}
|
| 51 |
+
iff = {id(s): i for i, s in enumerate(self.ff_slots)}
|
| 52 |
+
|
| 53 |
+
def d_out(s, x):
|
| 54 |
+
y = x @ s.weight() + s.module.bias[s.c0:s.c1]
|
| 55 |
+
return y * (self.l_out[io[id(s)]] - 1)
|
| 56 |
+
|
| 57 |
+
def d_ff(s, x):
|
| 58 |
+
return (x * (self.l_ff[iff[id(s)]] - 1)) @ s.weight()
|
| 59 |
+
|
| 60 |
+
self._h = attach_hooks(self.out_slots, d_out) + attach_hooks(self.ff_slots, d_ff)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class Prompt(_Adapter):
|
| 64 |
+
lr = 3e-2
|
| 65 |
+
|
| 66 |
+
def __init__(self, model, n, seed=0):
|
| 67 |
+
super().__init__()
|
| 68 |
+
self.model = [model]
|
| 69 |
+
emb = model.get_input_embeddings().weight
|
| 70 |
+
g = torch.Generator().manual_seed(seed)
|
| 71 |
+
ids = torch.randint(0, emb.shape[0], (n,), generator=g).to(emb.device)
|
| 72 |
+
self.n = n
|
| 73 |
+
self.P = nn.Parameter(emb[ids].detach().clone())
|
| 74 |
+
|
| 75 |
+
def logits(self, ids):
|
| 76 |
+
m = self.model[0]
|
| 77 |
+
e = torch.cat([self.P.expand(ids.shape[0], -1, -1), m.get_input_embeddings()(ids)], dim=1)
|
| 78 |
+
return m(inputs_embeds=e).logits[:, self.n:]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class LNTune(_Adapter):
|
| 82 |
+
lr = 1e-3
|
| 83 |
+
|
| 84 |
+
def __init__(self, model, layers=None, final=True):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.model = [model]
|
| 87 |
+
self.params = []
|
| 88 |
+
for name, mod in model.named_modules():
|
| 89 |
+
if isinstance(mod, nn.LayerNorm):
|
| 90 |
+
parts = name.split(".")
|
| 91 |
+
layer = next((int(p) for p in parts if p.isdigit()), None)
|
| 92 |
+
if (layer is None and final) or (layer is not None and (layers is None or layer in layers)):
|
| 93 |
+
for p in mod.parameters():
|
| 94 |
+
p.requires_grad_(True)
|
| 95 |
+
self.params.append(p)
|
| 96 |
+
|
| 97 |
+
def trainable(self):
|
| 98 |
+
return self.params
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
git+https://huggingface.co/Angshul/TensorPEFT
|
| 2 |
+
torch
|
| 3 |
+
transformers>=4.40
|
| 4 |
+
matplotlib
|
| 5 |
+
numpy
|