File size: 7,525 Bytes
29e60f4 ba9bed3 29e60f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | # TensorPEFT
---
license: apache-2.0
library_name: tensorpeft
tags:
- peft
- fine-tuning
- tensor-decomposition
- lora-alternative
---
Parameter-efficient fine-tuning with **tensor gradient-pursuit adapters**. TensorPEFT is a drop-in
alternative to LoRA for Hugging Face models, designed for very small trainable-parameter budgets.
LoRA gives every weight matrix its own low-rank update. TensorPEFT instead stacks all
equal-shaped matrices of the network into one tensor, for example
`layers x {q,k,v,o} x d_in x d_out`, and learns a few **rank-one CP atoms** of that tensor:
dW = sum_i a_i u_i(layer) o u_i(role) o u_i(in) o u_i(out)
Each atom couples every layer and projection while costing only
`L + P + d_in + d_out + 1` parameters. The atoms are not random. They are the leading rank-one
CP components of the negative gradient tensor (higher-order power iteration with deflation),
computed from **one gradient probe** before training. The coefficients start at zero, so the
model is unchanged at step 0, and every atom starts on a steepest-descent direction. After that,
training is ordinary: any optimiser and the HF `Trainer` both work.
## Install
pip install tensorpeft # once published; until then: pip install .
It requires `torch>=2.1`, `transformers>=4.40`, `safetensors` and `huggingface_hub`.
## Quick start
```python
from transformers import AutoModelForCausalLM
from tensorpeft import TensorPEFTConfig, get_tensorpeft_model, TensorPEFTModel
base = AutoModelForCausalLM.from_pretrained("gpt2")
model = get_tensorpeft_model(base, TensorPEFTConfig(atoms_per_group=4))
model.init_atoms(train_dataloader, n_batches=4) # the one step LoRA does not have
model.print_trainable_parameters() # trainable params: 37,036 || ... || 0.0298%
# train as usual: AdamW, lr ~3e-3, short warm-up, weight_decay 0 (or TensorPEFTTrainer below)
model.save_pretrained("my-adapter") # adapter_config.json + adapter_model.safetensors
model.push_to_hub("your-name/my-adapter")
base = AutoModelForCausalLM.from_pretrained("gpt2")
model = TensorPEFTModel.from_pretrained(base, "your-name/my-adapter") # local dir or Hub id
merged = model.merge_and_unload() # plain HF model, zero inference overhead
```
`train_dataloader` can yield dicts (`input_ids`, optional `attention_mask` / `labels`) or plain
input-id tensors. For causal LMs, labels default to the inputs.
### With the Hugging Face Trainer
```python
from tensorpeft import TensorPEFTTrainer
trainer = TensorPEFTTrainer(model=model, args=training_args, train_dataset=ds, data_collator=collator)
trainer.train() # checkpoints contain only the adapter
```
`TensorPEFTTrainer` is the stock `Trainer` except that it saves adapters instead of full model
weights. A complete script is in `examples/train_with_trainer.py`.
## Configuration
| `TensorPEFTConfig` field | default | meaning |
|---|---|---|
| `atoms_per_group` | 4 | rank-one atoms per tensor group; this sets the budget |
| `target_modules` | `"all-linear"` | or a list of name suffixes, e.g. `["q_proj", "v_proj"]` (`"c_attn.q"` for GPT-2) |
| `exclude_modules` | `lm_head`, embeddings | never adapted |
| `split_fused_qkv` | True | split GPT-2's fused `c_attn` into q, k and v |
| `init_batches` | 4 | batches used by `init_atoms` |
| `hopm_iters` | 30 | power-iteration sweeps per atom |
| `alpha_scale` | 33.3 | coefficient reparametrisation so that one learning rate (~3e-3) fits coefficients and factors |
**Budget.** Groups are formed automatically by matrix shape. GPT-2 small has three groups:
`{q,k,v,o}` (12x4x768x768), MLP-in (12x768x3072) and MLP-out (12x3072x768). One atom per group
is 9,259 parameters, four per group is 37,036, and six per group is 55,554.
`print_trainable_parameters()` shows the exact count for any model.
**Recipe.** Use AdamW at lr 3e-3 with about 10 warm-up steps, then a constant schedule and
weight decay 0. Use all atoms from the start: adding atoms gradually during training was clearly
worse in our tests.
## Results
Setup: GPT-2 124M (English base) fine-tuned on Bengali news (3M tokens), 300 AdamW steps,
batch 8 x 128 tokens. Validation cross-entropy is in nats/token, lower is better; the base model
scores 1.708. All methods saw identical data, and baseline learning rates follow their papers.
Three seeds, best baseline per budget:
| budget | TensorPEFT | best baseline | difference | seeds won | p (paired t) |
|---|---:|---|---:|:---:|---:|
| ~9k params | **1.3245 ± 0.004** | prompt tuning: 1.3684 ± 0.010 | **-0.044** | 3/3 | 0.008 |
| ~37k | 1.2558 ± 0.010 | LayerNorm tuning (38.4k): 1.2504 ± 0.003 | +0.005 | 1/3 | 0.37 (tie) |
| ~55k | **1.2368 ± 0.017** | (IA)³: 1.2880 ± 0.003 | **-0.051** | 3/3 | 0.026 |
All baselines at each budget (seed 0):
| budget | TensorPEFT | LoRA | QLoRA | VeRA | (IA)³ | prompt tuning | LayerNorm tuning | BitFit |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| ~1.5k | **1.461** | - | - | - | - | 1.493 (768 params) | 1.600 | - |
| ~9k | **1.327** | 1.430 | - | - | 1.432 | 1.370 | 1.407 | - |
| ~37k | 1.255 | 1.318 | 1.335 | 1.393 (24.6k) | - | 1.327 | **1.253** | - |
| ~55k | **1.231** | - | - | - | 1.289 | - | - | - |
| ~83k | - | - | - | - | - | - | - | 1.326 |
TensorPEFT budgets map to configs as follows: ~1.5k is one atom on the attention tensor only
(`target_modules=["c_attn.q", "c_attn.k", "c_attn.v", "attn.c_proj"]`, `atoms_per_group=1`),
~9k is `atoms_per_group=1`, ~37k is 4 and ~55k is 6. A dash means the method cannot be
configured at that budget on GPT-2, or was not run there.
LoRA's smallest all-layer configuration is r=1 on q,v (37k parameters). At 9k it is LoRA r=1 on
q,v of the last 3 layers.
**What this does and does not show.** TensorPEFT beats every baseline from about 1.5k to
about 9k parameters and at about 55k, and ties LayerNorm tuning at about 37k. With larger
budgets (≥ 115k) LoRA was better in our tests. The evidence covers one base model, one task and
short training. Results on other models and downstream tasks are not yet available.
`examples/reproduce_gpt2_bengali.py` reruns these benchmarks with this package
(`--atoms-per-group 1 | 4 | 6`). The
data comes from the Bengali news corpus at github.com/soham96/Bengali_news_classifier,
tokenised with the GPT-2 tokenizer.
## Supported models
Any model whose transformer blocks use `nn.Linear` or GPT-2-style `Conv1D` layers inside
numbered blocks (`...layers.N...` / `...h.N...`). Tested on GPT-2, Llama (GQA), Qwen2, Mistral,
Phi and Gemma configurations. Fused projections other than GPT-2's `c_attn` are treated as a
single matrix.
## Limitations (v0.1)
- **GPU support is untested.** The code is device- and dtype-agnostic (adapter parameters in
fp32, bf16/fp16 bases supported), but all tests and benchmarks so far ran on CPU.
- **Probe memory.** `init_atoms` needs one backward pass with gradients on the targeted weights,
plus an fp32 copy of each group's gradient tensor. That is roughly 2x the size of the targeted
weights, temporarily. Use `target_modules` to limit it for large models.
- **Quantised bases** (bitsandbytes 4/8-bit) are not supported yet, because the probe needs
weight gradients.
- **No checkpoint resume.** `Trainer` resume-from-checkpoint is not implemented; reload with
`TensorPEFTModel.from_pretrained(..., is_trainable=True)`.
## Tests
pip install .[test]
pytest tests
## License
Apache-2.0. Copyright 2026 Angshul Majumdar.
|