Simo76 commited on
Commit
fae8085
·
1 Parent(s): 3a213cf

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +135 -190
README.md CHANGED
@@ -1,191 +1,136 @@
1
- # Unified-LoRA
2
-
3
- **Adaptive LoRA fine-tuning with nested orbital rank control.**
4
-
5
- A closed-loop controller that dynamically adjusts LoRA rank during training based on observed stress, using a single adapter with sliced dimensions — no cold start, no capacity loss on transitions.
6
-
7
- ## Key results
8
-
9
- ### Stress test: task switch (MRPC SST-2, DistilBERT, 3 seeds)
10
-
11
- | | Baseline (r=16 fixed) | Unified (orbital) | Delta |
12
- |------------------------|-----------------------|-------------------|----------|
13
- | SST-2 Acc (new task) | 0.736 | 0.740 | **+0.004** |
14
- | MRPC F1 (retention) | 0.526 | 0.515 | -0.011 |
15
- | Effective rank | 16.0 | 13.6 | |
16
- | Rank saving | 0% | **15%** | |
17
-
18
- Under distribution shift, the controller adapts capacity dynamically with 15% rank saving and no performance loss.
19
-
20
- ### Rank trace under shock (Seed 1)
21
-
22
- ```
23
- [ 0] r4 r4 r4 r8 r8 r8 r8 r16 r16 r16 ground state → stress → ascend
24
- [ 10] r16 r16 r16 r16 r16 r16 r16 r16 r16 r16 ← MRPC at full capacity
25
- ...
26
- [ 60] <<<SHOCK r16 r16 r16 r16 r16 r16 r16 r16 ← task switch to SST-2
27
- [ 68] r8 r8 r8 r8 r8 r8 r4 r4 r4 r4 ← controller detects shift, descends
28
- [ 80] r4 r4 r4 r4 r4 r4 r4 r4 r4 r4 ← stable at ground state
29
- [ 92] r8 r16 r16 r16 r16 r16 r16 r16 r16 r16 new task needs capacity, re-ascends
30
- ```
31
-
32
- The controller exhibits **disturbance rejection**: detects the shock, descends to ground state, stabilizes, then re-ascends only when the new task demands capacity.
33
-
34
- ### Stable task (MRPC only, 120 steps, 3 seeds)
35
-
36
- | | Baseline (r=16) | Unified | Delta |
37
- |--------------|-----------------|---------|--------|
38
- | F1 mean | 0.818 | 0.820 | +0.002 |
39
- | σ | 0.008 | 0.008 | = |
40
-
41
- On stable training, the controller recognizes no intervention is needed and stays at r=16. Zero degradation.
42
-
43
- ## How it works
44
-
45
- ### Architecture: nested orbitals (r4 ⊂ r8 ⊂ r16)
46
-
47
- Unlike standard multi-adapter approaches (separate A/B matrices per rank), Unified-LoRA uses a **single pair** of matrices with rank controlled via slicing:
48
-
49
- ```python
50
- # One particle, multiple orbitals
51
- self.lora_A = Parameter(shape=[max_rank, in_features]) # shared
52
- self.lora_B = Parameter(shape=[out_features, max_rank]) # shared
53
-
54
- # Active rank = slice
55
- h = x @ A[:r, :].T # use first r rows
56
- delta = h @ B[:, :r].T # use first r columns
57
- ```
58
-
59
- When descending from r=16 to r=4, dimensions 0-3 retain all learned weights. Dimensions 4-15 are paused, not destroyed. When ascending back, they resume where they left off.
60
-
61
- **This solves the cold start problem** that caused F1 degradation in earlier versions with separate adapters.
62
-
63
- ### Controller: orbital trajectory with memory
64
-
65
- The controller implements closed-loop rank control:
66
-
67
- ```
68
- Stress → ascend to higher orbital, push delta to stack
69
- Stable → pop delta from stack, symmetric return
70
- Neutral → hold position, don't move
71
- ```
72
-
73
- The stress signal φ(t) combines loss deviation from EMA with spike detection:
74
-
75
- ```
76
- φ(t) = |loss - EMA(loss)| + 2.0 × max(0, loss - prev_loss)
77
- ```
78
-
79
- Thresholds are **adaptive** (μ ± kσ of recent φ history), so the controller auto-calibrates to any model/task scale without manual tuning.
80
-
81
- This is not a scheduler, not a rank budget, not a learning rate trick. It is a **trajectory controller** over model capacity.
82
-
83
- ## Quick start
84
-
85
- ```python
86
- from controller import setup_unified_lora, set_rank
87
-
88
- # One-call setup
89
- model, ctrl = setup_unified_lora(model, max_rank=16)
90
- optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
91
-
92
- # Training loop
93
- for step, batch in enumerate(train_loader):
94
- loss = model(**batch).loss
95
-
96
- new_rank = ctrl.step(loss.item())
97
- set_rank(model, new_rank)
98
-
99
- loss.backward()
100
- optimizer.step()
101
- optimizer.zero_grad()
102
- ```
103
-
104
- ## What works and what doesn't
105
-
106
- ### Works: distribution shift / noisy training
107
-
108
- Under task switch, label noise, or data corruption, the controller adapts rank dynamically. Demonstrated on:
109
-
110
- - **Task switch** (MRPC → SST-2): parity + 15% saving, disturbance rejection confirmed
111
- - **Label noise** (50%, DistilBERT/MRPC, 5 seeds): FSM switching F1=0.622 vs best fixed rank F1=0.439
112
-
113
- ### Works: black-box training (API / enterprise)
114
-
115
- The controller observes only loss trajectory — no access to gradients, internal activations, or optimizer state. Compatible with API-based fine-tuning endpoints where internal signals are not exposed.
116
-
117
- ### Doesn't help: clean stable training
118
-
119
- On standard GLUE tasks without perturbation, rank choice doesn't matter (r=8 ≈ r=16 ≈ r=32 from 67M to 3B parameters). The controller correctly recognizes this and stays at max rank — no harm, but no benefit.
120
-
121
- ## Experimental evolution
122
-
123
- This project tested many approaches. In the interest of scientific honesty:
124
-
125
- ### Tested and didn't help (clean data)
126
-
127
- - **Separate adapters per rank** (V1-V4): cold start on transitions caused 3-6 point F1 loss vs baseline. Each rank switch activated an adapter with independent weights that hadn't benefited from previous training. Solved by nested architecture.
128
- - **Adaptive rank per-layer** (gradient EMA): no performance benefit over fixed rank
129
- - **Fluid dynamics metrics** (shock, vorticity, swirl): too conservative as stress signals
130
- - **Trend-aware hysteresis** with fixed thresholds: controller either never activated or got stuck at intermediate rank
131
- - **Budget redistribution** across layers: winner-takes-all problem
132
-
133
- ### What works
134
-
135
- - **Nested orbital architecture**: zero cold start, parity with baseline guaranteed
136
- - **Trajectory controller with orbital memory**: disturbance rejection under task switch
137
- - **Adaptive thresholds** (μ ± kσ): auto-calibrates across models and tasks
138
- - **FSM adapter switching under noise**: measurably better performance and lower variance
139
-
140
- ## Computational overhead
141
-
142
- The controller adds O(1) computation per step: one EMA update, one threshold comparison, one stack operation. No SVD, no matrix decomposition. Negligible relative to the training step.
143
-
144
- ## Control-theoretic framing
145
-
146
- | Method | Control type | Rank dynamics |
147
- |-------------------------|-----------------|-----------------------|
148
- | Standard LoRA | None | rank = constant |
149
- | AdaLoRA | Open-loop | rank = f(step) |
150
- | **Unified-LoRA** | **Closed-loop** | rank = f(stress(t)) |
151
-
152
- Unified-LoRA introduces orbit-aware rank transitions: each capacity increase is tracked and reversed only under confirmed stability, preventing premature compression and oscillatory collapse.
153
-
154
- ## Repository structure
155
-
156
- ```
157
- controller.py # NestedLoRALinear + OrbitalController
158
- experiments/
159
- stress_test_task_switch.py # MRPC → SST-2 stress test (key result)
160
- stable_task_test.py # Single-task parity test
161
- docs/
162
- experimental_results.md # Detailed results and rank traces
163
- architecture.md # Nested orbital design
164
- notebooks/ # Experiment notebooks
165
- ```
166
-
167
- ## Open questions
168
-
169
- - Does nested orbital control scale to 7B+ models? (Tinker validation in progress)
170
- - What is the minimum shock magnitude that triggers measurable benefit?
171
- - Does adaptive LR control (black-box analog) show the same pattern on API platforms?
172
-
173
- ## Citation
174
-
175
- ```bibtex
176
- @software{unified_lora_2025,
177
- author = {Simona Vargiu},
178
- title = {Unified-LoRA: Adaptive Fine-Tuning with Nested Orbital Rank Control},
179
- year = {2025},
180
- url = {https://github.com/Sva76/Unified-LoRa}
181
  }
182
- ```
183
-
184
- ## Contact
185
-
186
- **Simona Vargiu** (Independent Researcher)
187
- For collaboration inquiries: simona.vargiu.malta@gmail.com
188
-
189
- ## License
190
-
191
- Apache License 2.0 — see [LICENSE](LICENSE) for details.
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Unified LoRA - MRPC Benchmark Example\n",
8
+ "\n",
9
+ "This notebook demonstrates Unified LoRA on the GLUE MRPC task.\n",
10
+ "\n",
11
+ "**Expected results:**\n",
12
+ "- Baseline LoRA: F1 ~0.78-0.79\n",
13
+ "- Unified LoRA: F1 ~0.78-0.79\n"
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "metadata": {},
19
+ "source": [
20
+ "!pip install -q transformers datasets peft evaluate scikit-learn accelerate"
21
+ ],
22
+ "outputs": [],
23
+ "execution_count": null
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "metadata": {},
28
+ "source": [
29
+ "import os\n",
30
+ "os.environ['WANDB_DISABLED'] = 'true'\n",
31
+ "\n",
32
+ "import torch\n",
33
+ "from datasets import load_dataset\n",
34
+ "from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments\n",
35
+ "from peft import LoraConfig, get_peft_model\n",
36
+ "from torch.utils.data import DataLoader\n",
37
+ "import evaluate\n",
38
+ "\n",
39
+ "from controller import UnifiedController\n",
40
+ "\n",
41
+ "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
42
+ "print(device)"
43
+ ],
44
+ "outputs": [],
45
+ "execution_count": null
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "metadata": {},
50
+ "source": [
51
+ "dataset = load_dataset('glue','mrpc')['train'].train_test_split(test_size=0.2, seed=42)\n",
52
+ "\n",
53
+ "model_name = 'distilbert-base-uncased'\n",
54
+ "tokenizer = AutoTokenizer.from_pretrained(model_name)\n",
55
+ "\n",
56
+ "def tokenize(ex):\n",
57
+ " return tokenizer(ex['sentence1'], ex['sentence2'], truncation=True, padding=True)\n",
58
+ "\n",
59
+ "train = dataset['train'].map(tokenize, batched=True).rename_column('label','labels')\n",
60
+ "test = dataset['test'].map(tokenize, batched=True).rename_column('label','labels')\n",
61
+ "\n",
62
+ "metric = evaluate.combine(['accuracy','f1'])\n",
63
+ "\n",
64
+ "def compute_metrics(p):\n",
65
+ " logits, labels = p\n",
66
+ " preds = torch.argmax(torch.tensor(logits), axis=-1)\n",
67
+ " return metric.compute(predictions=preds, references=labels)"
68
+ ],
69
+ "outputs": [],
70
+ "execution_count": null
71
+ },
72
+ {
73
+ "cell_type": "code",
74
+ "metadata": {},
75
+ "source": [
76
+ "# BASELINE\n",
77
+ "model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)\n",
78
+ "model = get_peft_model(model, LoraConfig(r=16, lora_alpha=32, target_modules=['q_lin','v_lin']))\n",
79
+ "\n",
80
+ "trainer = Trainer(\n",
81
+ " model=model,\n",
82
+ " train_dataset=train,\n",
83
+ " eval_dataset=test,\n",
84
+ " args=TrainingArguments(output_dir='./b', num_train_epochs=3, per_device_train_batch_size=16, fp16=True, report_to=None),\n",
85
+ " compute_metrics=compute_metrics\n",
86
+ ")\n",
87
+ "\n",
88
+ "trainer.train()\n",
89
+ "base = trainer.evaluate()"
90
+ ],
91
+ "outputs": [],
92
+ "execution_count": null
93
+ },
94
+ {
95
+ "cell_type": "code",
96
+ "metadata": {},
97
+ "source": [
98
+ "# UNIFIED\n",
99
+ "ctrl = UnifiedController()\n",
100
+ "\n",
101
+ "model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)\n",
102
+ "model = get_peft_model(model, LoraConfig(r=16, lora_alpha=32, target_modules=['q_lin','v_lin']))\n",
103
+ "model.to(device)\n",
104
+ "\n",
105
+ "loader = DataLoader(train.remove_columns(['sentence1','sentence2','idx']), batch_size=16, shuffle=True)\n",
106
+ "opt = torch.optim.AdamW(model.parameters(), lr=3e-5)\n",
107
+ "\n",
108
+ "model.train()\n",
109
+ "\n",
110
+ "for _ in range(3):\n",
111
+ " for batch in loader:\n",
112
+ " batch = {k:v.to(device) for k,v in batch.items() if k in ['input_ids','attention_mask','labels']}\n",
113
+ " out = model(**batch)\n",
114
+ " lr = ctrl.update(out.loss.item())\n",
115
+ " for g in opt.param_groups: g['lr'] = lr\n",
116
+ " out.loss.backward()\n",
117
+ " opt.step(); opt.zero_grad()\n",
118
+ "\n",
119
+ "model.eval()\n",
120
+ "trainer = Trainer(model=model, eval_dataset=test, args=TrainingArguments(output_dir='./u', per_device_eval_batch_size=16, fp16=True, report_to=None), compute_metrics=compute_metrics)\n",
121
+ "uni = trainer.evaluate()"
122
+ ],
123
+ "outputs": [],
124
+ "execution_count": null
125
+ }
126
+ ],
127
+ "metadata": {
128
+ "kernelspec": {
129
+ "display_name": "Python 3",
130
+ "language": "python",
131
+ "name": "python3"
132
+ }
133
+ },
134
+ "nbformat": 4,
135
+ "nbformat_minor": 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }