gbra 0.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of gbra might be problematic. Click here for more details.

gbra/__init__.py ADDED
@@ -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.0"
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
+ ]
gbra/callback.py ADDED
@@ -0,0 +1,23 @@
1
+ """Hugging Face Trainer integration."""
2
+
3
+ from transformers import TrainerCallback
4
+
5
+ from .model import refresh_norms
6
+
7
+
8
+ class GBRACallback(TrainerCallback):
9
+ """Refreshes GBRA norms every `refresh_every` optimizer steps (K).
10
+
11
+ Use refresh_every=None to never refresh (the norm stays at its initial value).
12
+
13
+ trainer = Trainer(..., callbacks=[GBRACallback(refresh_every=10)])
14
+ """
15
+
16
+ def __init__(self, refresh_every: int | None = 10):
17
+ if refresh_every is not None and refresh_every < 1:
18
+ raise ValueError("refresh_every must be >= 1 or None")
19
+ self.k = refresh_every
20
+
21
+ def on_step_end(self, args, state, control, model=None, **kwargs):
22
+ if self.k is not None and model is not None and state.global_step % self.k == 0:
23
+ refresh_norms(model)
gbra/layer.py ADDED
@@ -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}"
gbra/model.py ADDED
@@ -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,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: gbra
3
+ Version: 0.1.0
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: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: torch>=2.0
15
+ Requires-Dist: safetensors>=0.4
16
+ Provides-Extra: hf
17
+ Requires-Dist: transformers>=4.40; extra == "hf"
18
+ Provides-Extra: 4bit
19
+ Requires-Dist: bitsandbytes>=0.43; extra == "4bit"
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest; extra == "test"
22
+ Requires-Dist: transformers>=4.40; extra == "test"
23
+ Dynamic: license-file
24
+
25
+ # GBRA: Gember-Berke Rank Adaptation
26
+
27
+ *Pronounced "Gember RA". A GEMBER && Apex Logiç Studio project by Berke T. Adanç.*
28
+
29
+ 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.
30
+
31
+ ```
32
+ DoRA: W' = m · (W + s·BA) / ‖W + s·BA‖ ← norm recomputed every forward (and again during gradient-checkpoint recompute)
33
+ GBRA: W' = m · (W + s·BA) / n ← n is a stored buffer, refreshed every K optimizer steps
34
+ ```
35
+
36
+ **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.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install -e . # from this folder
42
+ pip install -e ".[hf]" # + Hugging Face Trainer callback
43
+ pip install -e ".[4bit]" # + 4-bit (bitsandbytes) base models
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from transformers import AutoModelForCausalLM, Trainer
50
+ from gbra import apply_gbra, GBRACallback, save_gbra, merge_gbra
51
+
52
+ model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
53
+ model = apply_gbra(model, r=16, alpha=16) # wraps q/k/v/o/gate/up/down projections
54
+
55
+ trainer = Trainer(model=model, ..., callbacks=[GBRACallback(refresh_every=10)])
56
+ trainer.train()
57
+
58
+ save_gbra(model, "my_adapter") # small adapter (A, B, m, n)
59
+ merge_gbra(model) # fold into plain nn.Linear for export
60
+ ```
61
+
62
+ Turkish aliases are also available: `gbra_ekle`, `tum_normlari_yenile`, `gbra_kaydet`, `gbra_yukle`, `gbra_birlestir`.
63
+
64
+ ## Preliminary results
65
+
66
+ 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.
67
+
68
+ | Method | s / step | vs LoRA | Peak memory | Val. loss |
69
+ |---|---|---|---|---|
70
+ | LoRA | **5.77** | 1.00× | 8.11 GB | 1.6376 |
71
+ | DoRA (PEFT) | 9.10 | 1.58× | 8.11 GB | 1.6285 |
72
+ | **GBRA K=10** | 6.47 | 1.12× | 8.11 GB | **1.6249** |
73
+ | GBRA K=100 | 6.47 | 1.12× | 8.11 GB | 1.6253 |
74
+ | GBRA K=never | 6.46 | 1.12× | 8.11 GB | 1.6263 |
75
+
76
+ **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.
77
+
78
+ ### Limitations (please read)
79
+
80
+ - **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.
81
+ - 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.
82
+ - The comparison is against PEFT's DoRA implementation at the time of testing, not against every possible DoRA implementation.
83
+ - 4-bit (bitsandbytes) support is implemented but **has not yet been benchmarked**.
84
+ - Unlike PEFT LoRA, GBRA does not currently use fused kernels such as Unsloth's.
85
+
86
+ ## Credits
87
+
88
+ 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.
89
+
90
+ 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.
91
+
92
+ ## License
93
+
94
+ MIT
95
+
96
+ ---
97
+
98
+ ## Türkçe özet
99
+
100
+ 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.
101
+
102
+ İ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,9 @@
1
+ gbra/__init__.py,sha256=dj5Ib35X7o9McgZ8bySFZd1AXUmeyWlaUZzh30GbacY,926
2
+ gbra/callback.py,sha256=EwxnSEHOnss0zmppHheOBtK2J0cmtQqJUnXh2yIvlpo,803
3
+ gbra/layer.py,sha256=L3rG-DR6d43PC9oE4cSh6wqktjOFVuSO0RrjpJNv7gM,3640
4
+ gbra/model.py,sha256=k3UR3QyrLEZsBqKmWsZdY1hyOrPpEO1XBMPF2npfclI,5126
5
+ gbra-0.1.0.dist-info/licenses/LICENSE,sha256=AJaZTTeRPlE_b0EgsutlchTPFn-JXY8Sdeg06uVmYXM,1101
6
+ gbra-0.1.0.dist-info/METADATA,sha256=ADTPdkZ5eGp7wsrWYgdw4tTkX5GS9hEU82Fk34UN6tE,5194
7
+ gbra-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ gbra-0.1.0.dist-info/top_level.txt,sha256=YS-1qm9iN_uHTzfqaocQiQ21mXfMWPLD-ZYDuCbSFQo,5
9
+ gbra-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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.
@@ -0,0 +1 @@
1
+ gbra