Simo76 commited on
Commit
9b36147
·
1 Parent(s): 55a084c

Revise README for clarity and additional details

Browse files

Updated README to clarify the adaptive rank controller's functionality and usage examples. Enhanced sections on results and limitations.

Files changed (1) hide show
  1. README.md +118 -95
README.md CHANGED
@@ -1,155 +1,178 @@
1
  # Unified-LoRA
2
 
3
- **Adaptive rank controller for LoRA fine-tuning.**
4
 
5
- A lightweight per-layer controller that dynamically adjusts LoRA rank during training based on gradient stress, eliminating manual rank selection.
6
 
7
- ## What it does
8
 
9
- Instead of fixing `rank=8` or `rank=16` and hoping it works, Unified-LoRA adapts the rank of each layer independently during training. Layers under stress get more capacity; stable layers get less. No grid search, no guessing.
 
10
 
11
- ## Results (multi-seed, 3 seeds)
12
-
13
- Evaluated on 3 GLUE tasks with DistilBERT-base-uncased, 3 epochs, LR=5e-4, α=16.
14
- Each result is mean ± std over 3 seeds.
15
 
16
- | Task | Metric | r=8 (fixed) | r=16 (fixed) | Unified (adaptive) | Avg Rank |
17
- |------|--------|-------------|--------------|-------------------|----------|
18
- | MRPC | F1 | **0.885 ± 0.007** | 0.882 ± 0.006 | 0.862 ± 0.025 | 9.1 |
19
- | CoLA | MCC | 0.474 ± 0.001 | **0.478 ± 0.011** | 0.477 ± 0.021 | 7.0 |
20
- | RTE | Accuracy | **0.560 ± 0.014** | 0.560 ± 0.018 | 0.543 ± 0.010 | 11.7 |
21
 
22
- **Summary:** The adaptive controller reduces average rank by 33-56% and produces interpretable per-layer rank patterns. Performance is within the noise margin of fixed-rank baselines on CoLA, but shows a gap on MRPC and RTE. The controller has higher variance than fixed-rank approaches.
 
23
 
24
- **Honest assessment:** At this scale (DistilBERT, GLUE), the choice between r=8 and r=16 makes little difference — the problem the controller tries to solve may not exist at small scale. Validation on larger models where rank selection matters more is needed.
 
 
25
 
26
  ## How it works
27
 
28
- Each LoRA adapter tracks the exponential moving average of its gradient norm. When the gradient stress increases (loss landscape is rough), the controller increases rank. When stress decreases (training is stable), rank is reduced.
29
 
30
  ```
31
- For each layer, at each step:
32
- 1. Compute grad_norm of LoRA parameters
33
- 2. Update EMA: stress = 0.9 * stress + 0.1 * grad_norm
34
- 3. If stress trend is increasing → rank += 2
35
- 4. If stress trend is decreasing → rank -= 2
36
- 5. Forward pass uses α/r scaling (standard LoRA)
37
  ```
38
 
39
- The controller adds ~30 lines of code and zero computational overhead beyond gradient norm computation.
40
 
41
- ## Per-layer behavior
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- The controller discovers meaningful patterns automatically:
44
 
45
- - **v_proj consistently needs more rank than q_proj** across all tasks
46
- - **Deep layers (4-5) need more rank** than early layers on complex tasks
47
- - **Easier tasks converge to lower rank** than harder tasks
 
 
48
 
49
- Example per-layer rank on MRPC:
50
  ```
 
51
  layer0.q: 7.9 layer0.v: 8.8
52
  layer1.q: 7.8 layer1.v: 7.9
53
  layer2.q: 7.9 layer2.v: 8.5
54
- layer3.q: 8.8 layer3.v: 11.3
55
- layer4.q: 10.3 layer4.v: 12.9
56
  layer5.q: 7.6 layer5.v: 11.3
57
  ```
58
 
59
- Rank trajectory over training (MRPC, seed=0):
60
- ```
61
- Step Avg Rank Loss
62
- 0 4.0 0.696
63
- 76 13.8 0.495
64
- 153 11.5 0.588
65
- 306 8.8 0.460
66
- 459 6.8 0.069
67
- 612 6.5 0.341
68
- 689 5.8 0.028
69
- ```
70
-
71
- The controller starts low, expands during early instability, then converges to lower rank as training stabilizes.
72
 
73
- ## What was tested and didn't improve results
 
 
 
 
74
 
75
- In the interest of scientific honesty, the following extensions were tested and did **not** outperform the simple Adaptive controller:
76
 
77
- - **Fluid dynamics metrics** (shock, vorticity, swirl as stress signal): controller became too conservative, suppressing rank across all tasks
78
- - **Budget redistribution** (fixed total rank budget shared across layers): "winner takes all" problem — high-stress layers starved low-stress layers
79
- - **Adaptive gradient clipping** driven by swirl: helped on small tasks (RTE +2.5%), hurt on large tasks (SST-2 -1.7%)
80
- - **Scaling without α/r**: performance came from implicit norm regulation, not true capacity control
81
 
82
- The simple version works best. Complexity did not pay.
83
 
84
- ## Comparison with existing methods
85
 
86
- | Method | Approach | Overhead | Difference |
87
- |--------|----------|----------|------------|
88
- | AdaLoRA | SVD importance scoring per layer | High (SVD each step) | Unified-LoRA is ~30 lines, zero SVD |
89
- | DyLoRA | Train on multiple ranks simultaneously | Medium | Runtime adaptation, not post-hoc |
90
- | Fixed LoRA | Manual rank selection | None | Unified-LoRA removes rank as a hyperparameter |
 
 
 
91
 
92
- Note: Direct numerical comparison with AdaLoRA was attempted but AdaLoRA did not function correctly in our setup (no rank pruning occurred). A fair comparison requires architecture-specific tuning of AdaLoRA scheduling parameters.
93
 
94
- ## Reproduce
95
 
96
- Run `benchmark.py` on Google Colab with a T4 GPU (~30 min):
97
 
98
- ```bash
99
- pip install transformers datasets evaluate accelerate scikit-learn
100
- python benchmark.py
101
- ```
102
 
103
- For multi-seed validation, run `validation_complete.py` (~15-20 min):
104
 
105
- ```bash
106
- python validation_complete.py
 
 
 
 
107
  ```
108
 
109
- ## Limitations
110
 
111
- - Validated on DistilBERT (67M) only at multi-seed level
112
- - At this scale, fixed r=8 performs comparably to r=16, limiting the potential benefit of adaptive rank
113
- - Higher variance than fixed-rank baselines
114
- - GLUE classification tasks only — no generation or instruction-following
115
- - Rank changes don't reduce peak memory (matrices allocated at max_rank)
116
- - Needs validation on larger models (3B-7B) where rank selection has more impact
117
 
118
- ## Two validated systems
 
119
 
120
- Unified-LoRA contains two complementary approaches, both validated:
 
 
 
 
 
 
 
121
 
122
- ### 1. FSM Mode Controller (φ(t))
123
 
124
- Validated on Tinker with Llama-3.2-1B. A finite state machine driven by a synaptic stress parameter φ(t) = f(C, E, S) that switches between three operational modes:
125
 
126
- - **Mode 0 (Single):** shared adapter, low stress < 0.3)
127
- - **Mode 1 (Multi):** task-specific adapters, moderate stress < 0.7)
128
- - **Mode 2 (Mirror):** stability snapshots, high stress 0.7)
 
 
 
129
 
130
- Demonstrated full stress → recovery cycle:
131
  ```
132
- [250] Mode=1 φ=0.333 (stable)
133
- SHOCK @ step 300
134
- [350] Mode=2 φ=0.827 (Mirror activated)
135
- RECOVERY @ step 500
136
- [550] Mode=1 φ=0.371 (return)
137
- [700] Mode=1 φ=0.333 (baseline restored)
138
  ```
139
 
140
- Key finding: φ returns to pre-shock regime after recovery (0.33 → 0.83 → 0.33), indicating reversible stress handling.
141
 
142
- ### 2. Per-layer Adaptive Rank Controller
 
143
 
144
- Validated on DistilBERT across 3 GLUE tasks with 3 seeds (results table above). Each layer independently adjusts its LoRA rank based on gradient stress EMA. Performance is within noise of fixed-rank baselines with 33-56% rank reduction.
 
145
 
146
- ### Evolution
 
 
147
 
148
- The project progressed from discrete mode switching (FSM) to continuous per-layer rank adaptation. Intermediate explorations included fluid dynamics metrics (shock, vorticity, swirl) and budget redistribution — these were tested rigorously but did not outperform the simple per-layer EMA approach. Details in "What was tested" above.
149
 
150
- ## Citation
 
 
 
 
151
 
152
- If you use this work:
153
 
154
  ```
155
  @software{unified_lora_2025,
 
1
  # Unified-LoRA
2
 
3
+ **Adaptive per-layer rank controller for LoRA fine-tuning.**
4
 
5
+ Automatically adjusts LoRA rank during training based on gradient stress. Eliminates rank as a hyperparameter.
6
 
7
+ ## Quick start
8
 
9
+ ```python
10
+ from unified_lora import inject_lora, get_lora_modules, setup_trainable
11
 
12
+ # Works with any model
13
+ model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased")
14
+ model = inject_lora(model, target_modules=["q_lin", "v_lin"])
15
+ model = setup_trainable(model)
16
 
17
+ # Standard training loop add one line
18
+ for batch in train_loader:
19
+ loss = model(**batch).loss
20
+ loss.backward()
21
+ clip_grad_norm_(model.parameters(), 1.0)
22
 
23
+ for m in get_lora_modules(model):
24
+ m.update_rank() # ← this is the controller
25
 
26
+ optimizer.step()
27
+ optimizer.zero_grad()
28
+ ```
29
 
30
  ## How it works
31
 
32
+ Each LoRA adapter tracks an EMA of its gradient norm. When stress increases, rank goes up. When stress decreases, rank goes down. Standard α/r scaling keeps the output magnitude stable across rank changes.
33
 
34
  ```
35
+ stress = 0.9 * stress + 0.1 * grad_norm
36
+ if stress_trend > threshold rank += 2
37
+ if stress_trend < -threshold rank -= 2
 
 
 
38
  ```
39
 
40
+ ~30 lines of code. Zero external dependencies beyond PyTorch.
41
 
42
+ ## Results (multi-seed, 3 seeds)
43
+
44
+ DistilBERT-base-uncased, 3 epochs, LR=5e-4, α=16:
45
+
46
+ | Task | Metric | r=8 (fixed) | r=16 (fixed) | Adaptive | Avg Rank |
47
+ |------|--------|-------------|--------------|----------|----------|
48
+ | MRPC | F1 | **0.885 ± 0.007** | 0.882 ± 0.006 | 0.862 ± 0.025 | 9.1 |
49
+ | CoLA | MCC | 0.474 ± 0.001 | **0.478 ± 0.011** | 0.477 ± 0.021 | 7.0 |
50
+ | RTE | Accuracy | **0.560 ± 0.014** | 0.560 ± 0.018 | 0.543 ± 0.010 | 11.7 |
51
+
52
+ ### What these results show
53
+
54
+ **The controller works mechanically.** It adapts rank, discovers per-layer patterns (v_proj needs more rank than q_proj, deep layers need more rank), and converges to lower rank over training.
55
 
56
+ **At this scale, it doesn't beat fixed rank.** On DistilBERT/GLUE, r=8 ≈ r=16 — the rank choice barely matters. The controller has higher variance than fixed-rank baselines.
57
 
58
+ **The hypothesis:** adaptive rank becomes valuable on larger models (3B-7B+) where the gap between r=8 and r=16 is significant. This has not been tested yet due to compute constraints.
59
+
60
+ ## Per-layer behavior
61
+
62
+ The controller discovers interpretable patterns consistently across seeds:
63
 
 
64
  ```
65
+ MRPC per-layer rank:
66
  layer0.q: 7.9 layer0.v: 8.8
67
  layer1.q: 7.8 layer1.v: 7.9
68
  layer2.q: 7.9 layer2.v: 8.5
69
+ layer3.q: 8.8 layer3.v: 11.3 ← deep v_proj needs more
70
+ layer4.q: 10.3 layer4.v: 12.9 ← deep v_proj needs more
71
  layer5.q: 7.6 layer5.v: 11.3
72
  ```
73
 
74
+ ## What was tested and didn't help
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ - **Fluid dynamics metrics** (shock, vorticity, swirl): too conservative
77
+ - **Budget redistribution** across layers: winner-takes-all problem
78
+ - **Adaptive gradient clipping** via swirl: inconsistent across tasks
79
+ - **Vincolo integration** (LR stability controller): zero shock events detected at this scale — training too stable to trigger
80
+ - **Predictive signals** (trend + acceleration): no improvement over simple EMA
81
 
82
+ The simplest controller works best. Every added complexity hurt or had no effect.
83
 
84
+ ## Two validated systems
 
 
 
85
 
86
+ ### 1. FSM Mode Controller φ(t)
87
 
88
+ Validated on Tinker with Llama-3.2-1B. Switches between Single/Multi/Mirror modes based on training stress:
89
 
90
+ ```
91
+ [250] Mode=1 φ=0.333 (stable)
92
+ SHOCK @ step 300
93
+ [350] Mode=2 φ=0.827 (Mirror activated)
94
+ RECOVERY @ step 500
95
+ [550] Mode=1 φ=0.371 (return)
96
+ [700] Mode=1 φ=0.333 (baseline restored)
97
+ ```
98
 
99
+ ### 2. Per-layer Adaptive Rank Controller
100
 
101
+ Validated on DistilBERT across 3 GLUE tasks with 3 seeds (results above).
102
 
103
+ ## Scaling to larger models
104
 
105
+ **This is the key open question.** The controller needs a setting where rank selection matters.
 
 
 
106
 
107
+ ### Test if rank matters on your model first
108
 
109
+ ```python
110
+ # If these three give very different results, the controller can help.
111
+ # If they're similar, rank doesn't matter and neither will the controller.
112
+ for r in [4, 8, 16]:
113
+ result = train_with_fixed_rank(model, rank=r)
114
+ print(f"r={r}: {result}")
115
  ```
116
 
117
+ ### Adapting to different architectures
118
 
119
+ ```python
120
+ # Llama / Mistral / Qwen
121
+ inject_lora(model, target_modules=["q_proj", "v_proj"])
 
 
 
122
 
123
+ # All attention projections
124
+ inject_lora(model, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"])
125
 
126
+ # With 4-bit quantization
127
+ model = AutoModelForCausalLM.from_pretrained(
128
+ "meta-llama/Llama-3.2-3B",
129
+ quantization_config=BitsAndBytesConfig(load_in_4bit=True),
130
+ device_map="auto",
131
+ )
132
+ inject_lora(model, target_modules=["q_proj", "v_proj"], max_r=32)
133
+ ```
134
 
135
+ ### What to report
136
 
137
+ If you test at larger scale, the key numbers are:
138
 
139
+ 1. **Does rank matter?** r=4 vs r=8 vs r=16 performance gap
140
+ 2. **Does adaptive match the best fixed rank?** Adaptive vs best-r
141
+ 3. **Variance:** mean ± std over3 seeds
142
+ 4. **Rank distribution:** per-layer average ranks
143
+
144
+ ## Repository structure
145
 
 
146
  ```
147
+ unified_lora.py # Controller module (drop-in)
148
+ benchmark.py # DistilBERT/GLUE benchmark
149
+ validation_complete.py # Multi-seed + ablation
150
+ controller.py # FSM controller φ(t) (legacy)
151
+ docs/ # Additional documentation
152
+ notebooks/ # Experiment notebooks
153
  ```
154
 
155
+ ## Reproduce
156
 
157
+ ```bash
158
+ pip install transformers datasets evaluate accelerate scikit-learn
159
 
160
+ # Single run (~30 min on T4)
161
+ python benchmark.py
162
 
163
+ # Multi-seed validation (~20 min on T4)
164
+ python validation_complete.py
165
+ ```
166
 
167
+ ## Limitations
168
 
169
+ - At DistilBERT/GLUE scale, fixed rank works equally well
170
+ - Higher variance than fixed-rank baselines
171
+ - Not tested on models > 1.1B at multi-seed level
172
+ - Classification tasks only — no generation evaluation
173
+ - Dynamic rank doesn't reduce peak memory
174
 
175
+ ## Citation
176
 
177
  ```
178
  @software{unified_lora_2025,