gbra 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gbra-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Berke T. Adanc (GEMBER && Apex Logic Studio)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gbra-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: gbra
3
+ Version: 0.1.1
4
+ Summary: GBRA (Gember-Berke Rank Adaptation): DoRA-style fine-tuning with a buffered, lazily refreshed weight norm.
5
+ Author: Berke T. Adanc
6
+ License: MIT
7
+ Keywords: lora,dora,peft,fine-tuning,llm
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: torch>=2.0
16
+ Requires-Dist: safetensors>=0.4
17
+ Provides-Extra: hf
18
+ Requires-Dist: transformers>=4.40; extra == "hf"
19
+ Provides-Extra: 4bit
20
+ Requires-Dist: bitsandbytes>=0.43; extra == "4bit"
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest; extra == "test"
23
+ Requires-Dist: transformers>=4.40; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # GBRA: Gember-Berke Rank Adaptation
27
+
28
+ *Pronounced "Gember RA". A GEMBER && Apex Logiç Studio project by Berke T. Adanç.*
29
+
30
+ GBRA is a DoRA-style parameter-efficient fine-tuning method. It keeps DoRA's split of each weight into a learned **magnitude** and a low-rank-updated **direction**, but **buffers the weight norm** instead of recomputing it in every forward pass.
31
+
32
+ ```
33
+ DoRA: W' = m · (W + s·BA) / ‖W + s·BA‖ ← norm recomputed every forward (and again during gradient-checkpoint recompute)
34
+ GBRA: W' = m · (W + s·BA) / n ← n is a stored buffer, refreshed every K optimizer steps
35
+ ```
36
+
37
+ **Why it helps:** With gradient accumulation, the weights only change once per optimizer step. Recomputing the norm for every micro-batch repeats the same work many times. GBRA computes it once and reuses it.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install gbra # core
43
+ pip install "gbra[hf]" # + Hugging Face Trainer callback
44
+ pip install "gbra[4bit]" # + 4-bit (bitsandbytes) base models
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from transformers import AutoModelForCausalLM, Trainer
51
+ from gbra import apply_gbra, GBRACallback, save_gbra, merge_gbra
52
+
53
+ model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
54
+ model = apply_gbra(model, r=16, alpha=16) # wraps q/k/v/o/gate/up/down projections
55
+
56
+ trainer = Trainer(model=model, ..., callbacks=[GBRACallback(refresh_every=10)])
57
+ trainer.train()
58
+
59
+ save_gbra(model, "my_adapter") # small adapter (A, B, m, n)
60
+ merge_gbra(model) # fold into plain nn.Linear for export
61
+ ```
62
+
63
+ Turkish aliases are also available: `gbra_ekle`, `tum_normlari_yenile`, `gbra_kaydet`, `gbra_yukle`, `gbra_birlestir`.
64
+
65
+ ## Preliminary results
66
+
67
+ Qwen2.5-0.5B-Instruct, 3,000 Turkish chat/tool-use examples, 375 optimizer steps (batch 1 × 8 accumulation), r=16, single Kaggle T4, fp16 autocast. Same data, seed and hyper-parameters for every method. DoRA and LoRA via 🤗 PEFT.
68
+
69
+ | Method | s / step | vs LoRA | Peak memory | Val. loss |
70
+ |---|---|---|---|---|
71
+ | LoRA | **5.77** | 1.00× | 8.11 GB | 1.6376 |
72
+ | DoRA (PEFT) | 9.10 | 1.58× | 8.11 GB | 1.6285 |
73
+ | **GBRA K=10** | 6.47 | 1.12× | 8.11 GB | **1.6249** |
74
+ | GBRA K=100 | 6.47 | 1.12× | 8.11 GB | 1.6253 |
75
+ | GBRA K=never | 6.46 | 1.12× | 8.11 GB | 1.6263 |
76
+
77
+ **In this run, GBRA was ~29% faster than DoRA per step, with equal or slightly lower validation loss.** A shorter 125-step run gave the same speed ratio.
78
+
79
+ ### Limitations (please read)
80
+
81
+ - **Single seed, single small model, single dataset.** The loss differences between GBRA variants and DoRA are small (≈0.001–0.004) and may be noise; repeated runs with more seeds are in progress.
82
+ - Much of the speed-up comes from **not recomputing the norm per micro-batch**. With no gradient accumulation and no gradient checkpointing, the gap to DoRA should be smaller.
83
+ - The comparison is against PEFT's DoRA implementation at the time of testing, not against every possible DoRA implementation.
84
+ - 4-bit (bitsandbytes) support is implemented but **has not yet been benchmarked**.
85
+ - Unlike PEFT LoRA, GBRA does not currently use fused kernels such as Unsloth's.
86
+
87
+ ## Credits
88
+
89
+ GBRA builds directly on **DoRA** (Liu et al., 2024, *"DoRA: Weight-Decomposed Low-Rank Adaptation"*) and **LoRA** (Hu et al., 2021). Related ideas on learned scaling vectors appear in (IA)³ and VeRA. The contribution here is the buffered / lazily refreshed norm and its evaluation.
90
+
91
+ The core `forward` of `GBRALinear` was written by Berke T. Adanç. Parts of the surrounding library and tests were developed with the help of an AI assistant.
92
+
93
+ ## Changelog
94
+
95
+ - **0.1.1**: Python 3.9 compatibility fix in `GBRACallback`; new `refresh_on_end` option (refreshes the norm once more when training ends); README install instructions for PyPI.
96
+ - **0.1.0**: First release.
97
+
98
+ ## License
99
+
100
+ MIT
101
+
102
+ ---
103
+
104
+ ## Türkçe özet
105
+
106
+ GBRA, DoRA'nın "büyüklük + yön" ayrımını korur, ama ağırlık normunu her adımda yeniden hesaplamak yerine **saklar** ve sadece **K optimizer adımında bir** günceller. Gradient birikimi kullanılan eğitimde ağırlıklar mini-batch'ler arasında değişmediği için DoRA aynı normu tekrar tekrar hesaplar. GBRA bu tekrarı ortadan kaldırır.
107
+
108
+ İlk deneyde (Qwen2.5-0.5B, 375 adım, tek seed) GBRA, DoRA'dan **~%29 daha hızlı** ve doğrulama loss'u aynı ya da biraz daha iyiydi. Sonuçlar tek seed'e dayandığı için **ön sonuç** olarak değerlendirilmelidir.
gbra-0.1.1/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # GBRA: Gember-Berke Rank Adaptation
2
+
3
+ *Pronounced "Gember RA". A GEMBER && Apex Logiç Studio project by Berke T. Adanç.*
4
+
5
+ GBRA is a DoRA-style parameter-efficient fine-tuning method. It keeps DoRA's split of each weight into a learned **magnitude** and a low-rank-updated **direction**, but **buffers the weight norm** instead of recomputing it in every forward pass.
6
+
7
+ ```
8
+ DoRA: W' = m · (W + s·BA) / ‖W + s·BA‖ ← norm recomputed every forward (and again during gradient-checkpoint recompute)
9
+ GBRA: W' = m · (W + s·BA) / n ← n is a stored buffer, refreshed every K optimizer steps
10
+ ```
11
+
12
+ **Why it helps:** With gradient accumulation, the weights only change once per optimizer step. Recomputing the norm for every micro-batch repeats the same work many times. GBRA computes it once and reuses it.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install gbra # core
18
+ pip install "gbra[hf]" # + Hugging Face Trainer callback
19
+ pip install "gbra[4bit]" # + 4-bit (bitsandbytes) base models
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```python
25
+ from transformers import AutoModelForCausalLM, Trainer
26
+ from gbra import apply_gbra, GBRACallback, save_gbra, merge_gbra
27
+
28
+ model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
29
+ model = apply_gbra(model, r=16, alpha=16) # wraps q/k/v/o/gate/up/down projections
30
+
31
+ trainer = Trainer(model=model, ..., callbacks=[GBRACallback(refresh_every=10)])
32
+ trainer.train()
33
+
34
+ save_gbra(model, "my_adapter") # small adapter (A, B, m, n)
35
+ merge_gbra(model) # fold into plain nn.Linear for export
36
+ ```
37
+
38
+ Turkish aliases are also available: `gbra_ekle`, `tum_normlari_yenile`, `gbra_kaydet`, `gbra_yukle`, `gbra_birlestir`.
39
+
40
+ ## Preliminary results
41
+
42
+ Qwen2.5-0.5B-Instruct, 3,000 Turkish chat/tool-use examples, 375 optimizer steps (batch 1 × 8 accumulation), r=16, single Kaggle T4, fp16 autocast. Same data, seed and hyper-parameters for every method. DoRA and LoRA via 🤗 PEFT.
43
+
44
+ | Method | s / step | vs LoRA | Peak memory | Val. loss |
45
+ |---|---|---|---|---|
46
+ | LoRA | **5.77** | 1.00× | 8.11 GB | 1.6376 |
47
+ | DoRA (PEFT) | 9.10 | 1.58× | 8.11 GB | 1.6285 |
48
+ | **GBRA K=10** | 6.47 | 1.12× | 8.11 GB | **1.6249** |
49
+ | GBRA K=100 | 6.47 | 1.12× | 8.11 GB | 1.6253 |
50
+ | GBRA K=never | 6.46 | 1.12× | 8.11 GB | 1.6263 |
51
+
52
+ **In this run, GBRA was ~29% faster than DoRA per step, with equal or slightly lower validation loss.** A shorter 125-step run gave the same speed ratio.
53
+
54
+ ### Limitations (please read)
55
+
56
+ - **Single seed, single small model, single dataset.** The loss differences between GBRA variants and DoRA are small (≈0.001–0.004) and may be noise; repeated runs with more seeds are in progress.
57
+ - Much of the speed-up comes from **not recomputing the norm per micro-batch**. With no gradient accumulation and no gradient checkpointing, the gap to DoRA should be smaller.
58
+ - The comparison is against PEFT's DoRA implementation at the time of testing, not against every possible DoRA implementation.
59
+ - 4-bit (bitsandbytes) support is implemented but **has not yet been benchmarked**.
60
+ - Unlike PEFT LoRA, GBRA does not currently use fused kernels such as Unsloth's.
61
+
62
+ ## Credits
63
+
64
+ GBRA builds directly on **DoRA** (Liu et al., 2024, *"DoRA: Weight-Decomposed Low-Rank Adaptation"*) and **LoRA** (Hu et al., 2021). Related ideas on learned scaling vectors appear in (IA)³ and VeRA. The contribution here is the buffered / lazily refreshed norm and its evaluation.
65
+
66
+ The core `forward` of `GBRALinear` was written by Berke T. Adanç. Parts of the surrounding library and tests were developed with the help of an AI assistant.
67
+
68
+ ## Changelog
69
+
70
+ - **0.1.1**: Python 3.9 compatibility fix in `GBRACallback`; new `refresh_on_end` option (refreshes the norm once more when training ends); README install instructions for PyPI.
71
+ - **0.1.0**: First release.
72
+
73
+ ## License
74
+
75
+ MIT
76
+
77
+ ---
78
+
79
+ ## Türkçe özet
80
+
81
+ GBRA, DoRA'nın "büyüklük + yön" ayrımını korur, ama ağırlık normunu her adımda yeniden hesaplamak yerine **saklar** ve sadece **K optimizer adımında bir** günceller. Gradient birikimi kullanılan eğitimde ağırlıklar mini-batch'ler arasında değişmediği için DoRA aynı normu tekrar tekrar hesaplar. GBRA bu tekrarı ortadan kaldırır.
82
+
83
+ İlk deneyde (Qwen2.5-0.5B, 375 adım, tek seed) GBRA, DoRA'dan **~%29 daha hızlı** ve doğrulama loss'u aynı ya da biraz daha iyiydi. Sonuçlar tek seed'e dayandığı için **ön sonuç** olarak değerlendirilmelidir.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gbra"
7
+ version = "0.1.1"
8
+ description = "GBRA (Gember-Berke Rank Adaptation): DoRA-style fine-tuning with a buffered, lazily refreshed weight norm."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Berke T. Adanc" }]
12
+ requires-python = ">=3.9"
13
+ dependencies = ["torch>=2.0", "safetensors>=0.4"]
14
+ keywords = ["lora", "dora", "peft", "fine-tuning", "llm"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ hf = ["transformers>=4.40"]
24
+ 4bit = ["bitsandbytes>=0.43"]
25
+ test = ["pytest", "transformers>=4.40"]
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
gbra-0.1.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ """GBRA - Gember-Berke Rank Adaptation ("Gember RA").
2
+
3
+ DoRA-style fine-tuning with a buffered (lazily refreshed) weight norm.
4
+ GEMBER && Apex Logic Studio - Berke T. Adanc
5
+ """
6
+
7
+ from .layer import GBRALinear
8
+ from .model import (
9
+ DEFAULT_TARGETS,
10
+ apply_gbra, refresh_norms, save_gbra, load_gbra, merge_gbra, gbra_state_dict,
11
+ gbra_ekle, tum_normlari_yenile, gbra_kaydet, gbra_yukle, gbra_birlestir,
12
+ )
13
+
14
+ __version__ = "0.1.1"
15
+
16
+
17
+ def __getattr__(name):
18
+ # GBRACallback needs transformers; import it lazily so the core works without it.
19
+ if name == "GBRACallback":
20
+ from .callback import GBRACallback
21
+ return GBRACallback
22
+ raise AttributeError(name)
23
+
24
+
25
+ __all__ = [
26
+ "GBRALinear", "GBRACallback", "DEFAULT_TARGETS",
27
+ "apply_gbra", "refresh_norms", "save_gbra", "load_gbra", "merge_gbra", "gbra_state_dict",
28
+ "gbra_ekle", "tum_normlari_yenile", "gbra_kaydet", "gbra_yukle", "gbra_birlestir",
29
+ ]
@@ -0,0 +1,32 @@
1
+ """Hugging Face Trainer integration."""
2
+
3
+ from typing import Optional
4
+
5
+ from transformers import TrainerCallback
6
+
7
+ from .model import refresh_norms
8
+
9
+
10
+ class GBRACallback(TrainerCallback):
11
+ """Refreshes GBRA norms every `refresh_every` optimizer steps (K).
12
+
13
+ Use refresh_every=None to never refresh during training (the norm stays at its
14
+ initial value). With refresh_on_end=True (default) the norm is refreshed once more
15
+ when training finishes, so a saved adapter always carries an up-to-date norm.
16
+
17
+ trainer = Trainer(..., callbacks=[GBRACallback(refresh_every=10)])
18
+ """
19
+
20
+ def __init__(self, refresh_every: Optional[int] = 10, refresh_on_end: bool = True):
21
+ if refresh_every is not None and refresh_every < 1:
22
+ raise ValueError("refresh_every must be >= 1 or None")
23
+ self.k = refresh_every
24
+ self.refresh_on_end = refresh_on_end
25
+
26
+ def on_step_end(self, args, state, control, model=None, **kwargs):
27
+ if self.k is not None and model is not None and state.global_step % self.k == 0:
28
+ refresh_norms(model)
29
+
30
+ def on_train_end(self, args, state, control, model=None, **kwargs):
31
+ if self.refresh_on_end and model is not None:
32
+ refresh_norms(model)
@@ -0,0 +1,97 @@
1
+ """GBRALinear: the core GBRA layer.
2
+
3
+ GBRA (Gember-Berke Rank Adaptation, read "Gember RA") keeps DoRA's
4
+ magnitude/direction split but buffers the column-norm instead of
5
+ recomputing it on every forward pass:
6
+
7
+ DoRA: W' = m * (W + s*B@A) / ||W + s*B@A|| (norm recomputed every forward)
8
+ GBRA: W' = m * (W + s*B@A) / n (n is a buffer, refreshed every K optimizer steps)
9
+
10
+ Norms are taken per output row (W has shape [out, in]).
11
+ """
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+
18
+ def _is_4bit(layer: nn.Module) -> bool:
19
+ return hasattr(getattr(layer, "weight", None), "quant_state")
20
+
21
+
22
+ def base_weight(layer: nn.Linear) -> torch.Tensor:
23
+ """Return the base weight as a dense float32 tensor (dequantizes 4-bit layers)."""
24
+ if _is_4bit(layer):
25
+ import bitsandbytes.functional as bnbF # optional dependency
26
+ return bnbF.dequantize_4bit(layer.weight.data, layer.weight.quant_state).float()
27
+ return layer.weight.detach().float()
28
+
29
+
30
+ class GBRALinear(nn.Module):
31
+ def __init__(self, taban: nn.Linear, r: int = 16, alpha: int = 16):
32
+ super().__init__()
33
+ if r <= 0:
34
+ raise ValueError("r must be a positive integer")
35
+ self.taban = taban # original layer (frozen)
36
+ for p in self.taban.parameters():
37
+ p.requires_grad = False
38
+
39
+ cikis, giris = taban.out_features, taban.in_features
40
+ self.r, self.alpha = r, alpha
41
+ self.s = alpha / r # LoRA scale
42
+ device = taban.weight.device
43
+
44
+ # LoRA matrices. B starts at zero, so at init GBRA == the original layer.
45
+ self.A = nn.Parameter(torch.empty(r, giris, device=device, dtype=torch.float32))
46
+ self.B = nn.Parameter(torch.zeros(cikis, r, device=device, dtype=torch.float32))
47
+ nn.init.kaiming_uniform_(self.A, a=5 ** 0.5)
48
+
49
+ # Magnitude vector m (learned), initialised to the row norm of W.
50
+ with torch.no_grad():
51
+ norm0 = base_weight(taban).norm(dim=1)
52
+ self.m = nn.Parameter(norm0.clone())
53
+
54
+ # Buffered norm n (NOT learned). Updated only by normu_yenile().
55
+ self.register_buffer("n", norm0.clone())
56
+
57
+ @torch.no_grad()
58
+ def normu_yenile(self):
59
+ """Lazy norm refresh: compute ||W + s*B@A|| for real and store it in n."""
60
+ W = base_weight(self.taban)
61
+ delta = self.s * (self.B.float() @ self.A.float())
62
+ self.n.copy_((W + delta).norm(dim=1))
63
+
64
+ refresh_norm = normu_yenile # English alias
65
+
66
+ def _ana(self, x):
67
+ """x @ W.T without bias (works for normal and 4-bit layers)."""
68
+ if _is_4bit(self.taban):
69
+ y = self.taban(x)
70
+ if self.taban.bias is not None:
71
+ y = y - self.taban.bias
72
+ return y
73
+ return F.linear(x, self.taban.weight, bias=None)
74
+
75
+ def forward(self, x):
76
+ # forward: Berke T. Adanc
77
+ # 1. Ana ağırlık: x @ W.T (bias eklemeden)
78
+ ana = self._ana(x)
79
+
80
+ # 2. LoRA: önce A, sonra B
81
+ lora_a = F.linear(x, self.A.to(x.dtype), bias=None)
82
+ lora_b = F.linear(lora_a, self.B.to(x.dtype), bias=None)
83
+
84
+ # 3. Ölçek: m / n, ardından x'in dtype'ına dönüştür
85
+ olcek = (self.m / self.n).to(x.dtype)
86
+
87
+ # 4. Ana ağırlık + LoRA toplamını ölçekle
88
+ y = olcek * (ana + self.s * lora_b)
89
+
90
+ # 5. Bias'ı en son ekle (None olabilir)
91
+ if self.taban.bias is not None:
92
+ y = y + self.taban.bias
93
+
94
+ return y
95
+
96
+ def extra_repr(self):
97
+ return f"in={self.taban.in_features}, out={self.taban.out_features}, r={self.r}, alpha={self.alpha}"
@@ -0,0 +1,123 @@
1
+ """Model-level helpers: attach GBRA, refresh norms, merge, save and load."""
2
+
3
+ import json
4
+ import os
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+
9
+ from .layer import GBRALinear, base_weight
10
+
11
+ DEFAULT_TARGETS = ("q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj")
12
+ CONFIG_NAME = "gbra_config.json"
13
+ WEIGHTS_NAME = "gbra_adapter.safetensors"
14
+
15
+
16
+ def apply_gbra(model: nn.Module, targets=DEFAULT_TARGETS, r: int = 16, alpha: int = 16,
17
+ verbose: bool = True) -> nn.Module:
18
+ """Freeze the model and wrap every target nn.Linear (incl. 4-bit) with GBRALinear."""
19
+ for p in model.parameters():
20
+ p.requires_grad = False
21
+ count = 0
22
+ for module in list(model.modules()):
23
+ for name, child in list(module.named_children()):
24
+ if name in targets and isinstance(child, nn.Linear) and not isinstance(child, GBRALinear):
25
+ setattr(module, name, GBRALinear(child, r=r, alpha=alpha))
26
+ count += 1
27
+ if count == 0:
28
+ raise ValueError(f"No target layers found. Check the layer names of your model. targets={targets}")
29
+ model._gbra_config = {"r": r, "alpha": alpha, "targets": list(targets)}
30
+ if verbose:
31
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
32
+ total = sum(p.numel() for p in model.parameters())
33
+ print(f"GBRA: {count} layers | trainable {trainable:,} / {total:,} ({100 * trainable / total:.2f}%)")
34
+ return model
35
+
36
+
37
+ @torch.no_grad()
38
+ def refresh_norms(model: nn.Module) -> int:
39
+ """Refresh the buffered norm of every GBRA layer. Returns the number of layers."""
40
+ n = 0
41
+ for m in model.modules():
42
+ if isinstance(m, GBRALinear):
43
+ m.normu_yenile()
44
+ n += 1
45
+ return n
46
+
47
+
48
+ def gbra_state_dict(model: nn.Module) -> dict:
49
+ """Only the GBRA tensors (A, B, m, n) - small enough to share as an adapter."""
50
+ out = {}
51
+ for name, m in model.named_modules():
52
+ if isinstance(m, GBRALinear):
53
+ for key in ("A", "B", "m", "n"):
54
+ out[f"{name}.{key}"] = getattr(m, key).detach().cpu().contiguous()
55
+ return out
56
+
57
+
58
+ def save_gbra(model: nn.Module, path: str):
59
+ os.makedirs(path, exist_ok=True)
60
+ cfg = getattr(model, "_gbra_config", None)
61
+ if cfg is None:
62
+ raise ValueError("This model has no GBRA layers (call apply_gbra first).")
63
+ with open(os.path.join(path, CONFIG_NAME), "w", encoding="utf-8") as f:
64
+ json.dump({"format": "gbra", "version": 1, **cfg}, f, indent=2)
65
+ from safetensors.torch import save_file
66
+ save_file(gbra_state_dict(model), os.path.join(path, WEIGHTS_NAME))
67
+
68
+
69
+ def load_gbra(model: nn.Module, path: str) -> nn.Module:
70
+ """Attach GBRA to a fresh base model and load adapter weights saved with save_gbra."""
71
+ with open(os.path.join(path, CONFIG_NAME), encoding="utf-8") as f:
72
+ cfg = json.load(f)
73
+ if not any(isinstance(m, GBRALinear) for m in model.modules()):
74
+ apply_gbra(model, targets=tuple(cfg["targets"]), r=cfg["r"], alpha=cfg["alpha"], verbose=False)
75
+ from safetensors.torch import load_file
76
+ tensors = load_file(os.path.join(path, WEIGHTS_NAME))
77
+ modules = dict(model.named_modules())
78
+ used = set()
79
+ for full, t in tensors.items():
80
+ name, key = full.rsplit(".", 1)
81
+ layer = modules.get(name)
82
+ if not isinstance(layer, GBRALinear):
83
+ raise KeyError(f"Adapter tensor {full} has no matching GBRA layer in this model.")
84
+ getattr(layer, key).data.copy_(t.to(getattr(layer, key).device))
85
+ used.add(name)
86
+ missing = [n for n, m in modules.items() if isinstance(m, GBRALinear) and n not in used]
87
+ if missing:
88
+ raise KeyError(f"No adapter weights for {len(missing)} GBRA layers, e.g. {missing[0]}")
89
+ return model
90
+
91
+
92
+ @torch.no_grad()
93
+ def merge_gbra(model: nn.Module, dtype=None) -> nn.Module:
94
+ """Fold every GBRA layer into a plain nn.Linear: W' = (m/n) * (W + s*B@A).
95
+
96
+ The merged model gives the same outputs as the GBRA model (using the current
97
+ buffered norm). 4-bit layers are dequantized, so the result is a dense model
98
+ you can export (e.g. to GGUF).
99
+ """
100
+ for module in list(model.modules()):
101
+ for name, child in list(module.named_children()):
102
+ if isinstance(child, GBRALinear):
103
+ W = base_weight(child.taban)
104
+ Wm = (child.m / child.n)[:, None] * (W + child.s * child.B.float() @ child.A.float())
105
+ bias = child.taban.bias
106
+ out_dtype = dtype or (bias.dtype if bias is not None else torch.float32)
107
+ lin = nn.Linear(Wm.shape[1], Wm.shape[0], bias=bias is not None,
108
+ device=Wm.device, dtype=out_dtype)
109
+ lin.weight.copy_(Wm.to(out_dtype))
110
+ if bias is not None:
111
+ lin.bias.copy_(bias.detach().to(out_dtype))
112
+ setattr(module, name, lin)
113
+ if hasattr(model, "_gbra_config"):
114
+ del model._gbra_config
115
+ return model
116
+
117
+
118
+ # Turkish aliases
119
+ gbra_ekle = apply_gbra
120
+ tum_normlari_yenile = refresh_norms
121
+ gbra_kaydet = save_gbra
122
+ gbra_yukle = load_gbra
123
+ gbra_birlestir = merge_gbra
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: gbra
3
+ Version: 0.1.1
4
+ Summary: GBRA (Gember-Berke Rank Adaptation): DoRA-style fine-tuning with a buffered, lazily refreshed weight norm.
5
+ Author: Berke T. Adanc
6
+ License: MIT
7
+ Keywords: lora,dora,peft,fine-tuning,llm
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: torch>=2.0
16
+ Requires-Dist: safetensors>=0.4
17
+ Provides-Extra: hf
18
+ Requires-Dist: transformers>=4.40; extra == "hf"
19
+ Provides-Extra: 4bit
20
+ Requires-Dist: bitsandbytes>=0.43; extra == "4bit"
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest; extra == "test"
23
+ Requires-Dist: transformers>=4.40; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # GBRA: Gember-Berke Rank Adaptation
27
+
28
+ *Pronounced "Gember RA". A GEMBER && Apex Logiç Studio project by Berke T. Adanç.*
29
+
30
+ GBRA is a DoRA-style parameter-efficient fine-tuning method. It keeps DoRA's split of each weight into a learned **magnitude** and a low-rank-updated **direction**, but **buffers the weight norm** instead of recomputing it in every forward pass.
31
+
32
+ ```
33
+ DoRA: W' = m · (W + s·BA) / ‖W + s·BA‖ ← norm recomputed every forward (and again during gradient-checkpoint recompute)
34
+ GBRA: W' = m · (W + s·BA) / n ← n is a stored buffer, refreshed every K optimizer steps
35
+ ```
36
+
37
+ **Why it helps:** With gradient accumulation, the weights only change once per optimizer step. Recomputing the norm for every micro-batch repeats the same work many times. GBRA computes it once and reuses it.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install gbra # core
43
+ pip install "gbra[hf]" # + Hugging Face Trainer callback
44
+ pip install "gbra[4bit]" # + 4-bit (bitsandbytes) base models
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ from transformers import AutoModelForCausalLM, Trainer
51
+ from gbra import apply_gbra, GBRACallback, save_gbra, merge_gbra
52
+
53
+ model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
54
+ model = apply_gbra(model, r=16, alpha=16) # wraps q/k/v/o/gate/up/down projections
55
+
56
+ trainer = Trainer(model=model, ..., callbacks=[GBRACallback(refresh_every=10)])
57
+ trainer.train()
58
+
59
+ save_gbra(model, "my_adapter") # small adapter (A, B, m, n)
60
+ merge_gbra(model) # fold into plain nn.Linear for export
61
+ ```
62
+
63
+ Turkish aliases are also available: `gbra_ekle`, `tum_normlari_yenile`, `gbra_kaydet`, `gbra_yukle`, `gbra_birlestir`.
64
+
65
+ ## Preliminary results
66
+
67
+ Qwen2.5-0.5B-Instruct, 3,000 Turkish chat/tool-use examples, 375 optimizer steps (batch 1 × 8 accumulation), r=16, single Kaggle T4, fp16 autocast. Same data, seed and hyper-parameters for every method. DoRA and LoRA via 🤗 PEFT.
68
+
69
+ | Method | s / step | vs LoRA | Peak memory | Val. loss |
70
+ |---|---|---|---|---|
71
+ | LoRA | **5.77** | 1.00× | 8.11 GB | 1.6376 |
72
+ | DoRA (PEFT) | 9.10 | 1.58× | 8.11 GB | 1.6285 |
73
+ | **GBRA K=10** | 6.47 | 1.12× | 8.11 GB | **1.6249** |
74
+ | GBRA K=100 | 6.47 | 1.12× | 8.11 GB | 1.6253 |
75
+ | GBRA K=never | 6.46 | 1.12× | 8.11 GB | 1.6263 |
76
+
77
+ **In this run, GBRA was ~29% faster than DoRA per step, with equal or slightly lower validation loss.** A shorter 125-step run gave the same speed ratio.
78
+
79
+ ### Limitations (please read)
80
+
81
+ - **Single seed, single small model, single dataset.** The loss differences between GBRA variants and DoRA are small (≈0.001–0.004) and may be noise; repeated runs with more seeds are in progress.
82
+ - Much of the speed-up comes from **not recomputing the norm per micro-batch**. With no gradient accumulation and no gradient checkpointing, the gap to DoRA should be smaller.
83
+ - The comparison is against PEFT's DoRA implementation at the time of testing, not against every possible DoRA implementation.
84
+ - 4-bit (bitsandbytes) support is implemented but **has not yet been benchmarked**.
85
+ - Unlike PEFT LoRA, GBRA does not currently use fused kernels such as Unsloth's.
86
+
87
+ ## Credits
88
+
89
+ GBRA builds directly on **DoRA** (Liu et al., 2024, *"DoRA: Weight-Decomposed Low-Rank Adaptation"*) and **LoRA** (Hu et al., 2021). Related ideas on learned scaling vectors appear in (IA)³ and VeRA. The contribution here is the buffered / lazily refreshed norm and its evaluation.
90
+
91
+ The core `forward` of `GBRALinear` was written by Berke T. Adanç. Parts of the surrounding library and tests were developed with the help of an AI assistant.
92
+
93
+ ## Changelog
94
+
95
+ - **0.1.1**: Python 3.9 compatibility fix in `GBRACallback`; new `refresh_on_end` option (refreshes the norm once more when training ends); README install instructions for PyPI.
96
+ - **0.1.0**: First release.
97
+
98
+ ## License
99
+
100
+ MIT
101
+
102
+ ---
103
+
104
+ ## Türkçe özet
105
+
106
+ GBRA, DoRA'nın "büyüklük + yön" ayrımını korur, ama ağırlık normunu her adımda yeniden hesaplamak yerine **saklar** ve sadece **K optimizer adımında bir** günceller. Gradient birikimi kullanılan eğitimde ağırlıklar mini-batch'ler arasında değişmediği için DoRA aynı normu tekrar tekrar hesaplar. GBRA bu tekrarı ortadan kaldırır.
107
+
108
+ İlk deneyde (Qwen2.5-0.5B, 375 adım, tek seed) GBRA, DoRA'dan **~%29 daha hızlı** ve doğrulama loss'u aynı ya da biraz daha iyiydi. Sonuçlar tek seed'e dayandığı için **ön sonuç** olarak değerlendirilmelidir.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/gbra/__init__.py
5
+ src/gbra/callback.py
6
+ src/gbra/layer.py
7
+ src/gbra/model.py
8
+ src/gbra.egg-info/PKG-INFO
9
+ src/gbra.egg-info/SOURCES.txt
10
+ src/gbra.egg-info/dependency_links.txt
11
+ src/gbra.egg-info/requires.txt
12
+ src/gbra.egg-info/top_level.txt
13
+ tests/test_gbra.py
@@ -0,0 +1,12 @@
1
+ torch>=2.0
2
+ safetensors>=0.4
3
+
4
+ [4bit]
5
+ bitsandbytes>=0.43
6
+
7
+ [hf]
8
+ transformers>=4.40
9
+
10
+ [test]
11
+ pytest
12
+ transformers>=4.40
@@ -0,0 +1 @@
1
+ gbra
@@ -0,0 +1,202 @@
1
+ import torch
2
+ import torch.nn as nn
3
+ import pytest
4
+
5
+ from gbra import (GBRALinear, apply_gbra, refresh_norms, save_gbra, load_gbra,
6
+ merge_gbra, GBRACallback)
7
+
8
+ TOL = 1e-5
9
+
10
+
11
+ def dora_reference(layer: GBRALinear, x):
12
+ """DoRA formula: the norm is recomputed from scratch every time."""
13
+ W = layer.taban.weight
14
+ V = W + layer.s * layer.B @ layer.A
15
+ y = x @ (layer.m[:, None] * V / V.norm(dim=1)[:, None]).T
16
+ return y + layer.taban.bias if layer.taban.bias is not None else y
17
+
18
+
19
+ @pytest.fixture
20
+ def layer():
21
+ torch.manual_seed(0)
22
+ return GBRALinear(nn.Linear(32, 24), r=4, alpha=8)
23
+
24
+
25
+ def perturb(layer):
26
+ with torch.no_grad():
27
+ layer.B.normal_(0, 0.1)
28
+ layer.m.mul_(1.3)
29
+
30
+
31
+ def test_identity_at_init(layer):
32
+ x = torch.randn(5, 32)
33
+ assert torch.allclose(layer(x), layer.taban(x), atol=TOL)
34
+
35
+
36
+ def test_equals_dora_after_refresh(layer):
37
+ perturb(layer)
38
+ layer.normu_yenile()
39
+ x = torch.randn(5, 32)
40
+ assert torch.allclose(layer(x), dora_reference(layer, x), atol=TOL)
41
+
42
+
43
+ def test_uses_buffered_norm_before_refresh(layer):
44
+ perturb(layer)
45
+ layer.normu_yenile()
46
+ with torch.no_grad():
47
+ layer.B.add_(0.05)
48
+ x = torch.randn(5, 32)
49
+ W, b = layer.taban.weight, layer.taban.bias
50
+ expected = (layer.m / layer.n) * (x @ W.T + layer.s * (x @ layer.A.T) @ layer.B.T) + b
51
+ assert torch.allclose(layer(x), expected, atol=TOL)
52
+ assert not torch.allclose(layer(x), dora_reference(layer, x), atol=TOL)
53
+
54
+
55
+ def test_no_bias_and_3d_input():
56
+ l = GBRALinear(nn.Linear(32, 24, bias=False), r=4)
57
+ x = torch.randn(2, 7, 32)
58
+ assert l(x).shape == (2, 7, 24)
59
+ assert torch.allclose(l(x), l.taban(x), atol=TOL)
60
+
61
+
62
+ def test_gradients(layer):
63
+ perturb(layer)
64
+ layer(torch.randn(5, 32)).sum().backward()
65
+ for p in (layer.A, layer.B, layer.m):
66
+ assert p.grad is not None and p.grad.abs().sum() > 0
67
+ assert layer.taban.weight.grad is None
68
+ assert not layer.n.requires_grad
69
+
70
+
71
+ def test_magnitude_scales_output(layer):
72
+ """Berke'nin testi: m iki katina cikinca (bias'siz) cikti da iki katina cikmali."""
73
+ l = GBRALinear(nn.Linear(32, 24, bias=False), r=4)
74
+ x = torch.randn(3, 32)
75
+ y1 = l(x)
76
+ with torch.no_grad():
77
+ l.m.mul_(2)
78
+ assert torch.allclose(l(x), 2 * y1, atol=TOL)
79
+
80
+
81
+ def test_rejects_bad_rank():
82
+ with pytest.raises(ValueError):
83
+ GBRALinear(nn.Linear(4, 4), r=0)
84
+
85
+
86
+ # ---------------- model-level ----------------
87
+
88
+ class Tiny(nn.Module):
89
+ def __init__(self):
90
+ super().__init__()
91
+ self.q_proj = nn.Linear(16, 16)
92
+ self.up_proj = nn.Linear(16, 32, bias=False)
93
+ self.other = nn.Linear(32, 8)
94
+
95
+ def forward(self, x):
96
+ return self.other(torch.relu(self.up_proj(self.q_proj(x))))
97
+
98
+
99
+ def trained_tiny():
100
+ torch.manual_seed(1)
101
+ m = apply_gbra(Tiny(), targets=("q_proj", "up_proj"), r=4, alpha=4, verbose=False)
102
+ for mod in m.modules():
103
+ if isinstance(mod, GBRALinear):
104
+ perturb(mod)
105
+ refresh_norms(m)
106
+ return m
107
+
108
+
109
+ def test_apply_only_targets_and_freezes():
110
+ m = apply_gbra(Tiny(), targets=("q_proj", "up_proj"), verbose=False)
111
+ assert isinstance(m.q_proj, GBRALinear) and isinstance(m.up_proj, GBRALinear)
112
+ assert isinstance(m.other, nn.Linear) and not isinstance(m.other, GBRALinear)
113
+ assert not any(p.requires_grad for p in m.other.parameters())
114
+
115
+
116
+ def test_apply_raises_when_nothing_matches():
117
+ with pytest.raises(ValueError):
118
+ apply_gbra(Tiny(), targets=("nope",), verbose=False)
119
+
120
+
121
+ def test_refresh_norms_count():
122
+ assert refresh_norms(trained_tiny()) == 2
123
+
124
+
125
+ def test_merge_matches_gbra_output():
126
+ m = trained_tiny()
127
+ x = torch.randn(4, 16)
128
+ before = m(x)
129
+ merge_gbra(m)
130
+ assert not any(isinstance(mod, GBRALinear) for mod in m.modules())
131
+ assert torch.allclose(m(x), before, atol=1e-5)
132
+
133
+
134
+ def test_save_load_roundtrip(tmp_path):
135
+ m = trained_tiny()
136
+ x = torch.randn(4, 16)
137
+ expected = m(x)
138
+ save_gbra(m, tmp_path)
139
+ torch.manual_seed(1)
140
+ fresh = Tiny()
141
+ # same base weights as the trained model's base layers
142
+ fresh.q_proj.load_state_dict(m.q_proj.taban.state_dict())
143
+ fresh.up_proj.load_state_dict(m.up_proj.taban.state_dict())
144
+ fresh.other.load_state_dict(m.other.state_dict())
145
+ load_gbra(fresh, tmp_path)
146
+ assert torch.allclose(fresh(x), expected, atol=TOL)
147
+
148
+
149
+ def test_callback_refreshes_every_k():
150
+ m = trained_tiny()
151
+ with torch.no_grad():
152
+ m.q_proj.B.add_(0.1)
153
+ old = m.q_proj.n.clone()
154
+
155
+ class S: # minimal TrainerState stand-in
156
+ global_step = 3
157
+ cb = GBRACallback(refresh_every=5)
158
+ cb.on_step_end(None, S, None, model=m)
159
+ assert torch.equal(m.q_proj.n, old) # 3 % 5 != 0 -> no refresh
160
+ S.global_step = 5
161
+ cb.on_step_end(None, S, None, model=m)
162
+ assert not torch.equal(m.q_proj.n, old) # refreshed
163
+
164
+
165
+ def test_qwen2_tiny_trains():
166
+ transformers = pytest.importorskip("transformers")
167
+ cfg = transformers.Qwen2Config(vocab_size=200, hidden_size=64, intermediate_size=128,
168
+ num_hidden_layers=2, num_attention_heads=4, num_key_value_heads=2)
169
+ torch.manual_seed(0)
170
+ model = transformers.Qwen2ForCausalLM(cfg)
171
+ model.gradient_checkpointing_enable()
172
+ model.enable_input_require_grads()
173
+ model.config.use_cache = False
174
+ apply_gbra(model, r=4, alpha=4, verbose=False)
175
+ x = torch.randint(0, 200, (1, 32))
176
+ opt = torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=1e-2)
177
+ first = model(input_ids=x, labels=x).loss.item()
178
+ for _ in range(10):
179
+ loss = model(input_ids=x, labels=x).loss
180
+ loss.backward(); opt.step(); opt.zero_grad()
181
+ refresh_norms(model)
182
+ assert loss.item() < first
183
+
184
+
185
+ def test_callback_refresh_on_end():
186
+ m = trained_tiny()
187
+ with torch.no_grad():
188
+ m.q_proj.B.add_(0.1)
189
+ old = m.q_proj.n.clone()
190
+ GBRACallback(refresh_every=None).on_train_end(None, None, None, model=m)
191
+ assert not torch.equal(m.q_proj.n, old)
192
+ m2 = trained_tiny()
193
+ with torch.no_grad():
194
+ m2.q_proj.B.add_(0.1)
195
+ old2 = m2.q_proj.n.clone()
196
+ GBRACallback(refresh_every=None, refresh_on_end=False).on_train_end(None, None, None, model=m2)
197
+ assert torch.equal(m2.q_proj.n, old2)
198
+
199
+
200
+ def test_version():
201
+ import gbra
202
+ assert gbra.__version__ == "0.1.1"