gbra 0.1.1__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.
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.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
+ ]
gbra/callback.py ADDED
@@ -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)
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,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,9 @@
1
+ gbra/__init__.py,sha256=nzgnXtcnsiZb7N_sUsbdi82vv9OU9uFbKcDAkaGC3GM,926
2
+ gbra/callback.py,sha256=K-pM2KFZidPsx9x-AB-iiRjtHYz5jT_f0U2S3HyA6zY,1238
3
+ gbra/layer.py,sha256=L3rG-DR6d43PC9oE4cSh6wqktjOFVuSO0RrjpJNv7gM,3640
4
+ gbra/model.py,sha256=k3UR3QyrLEZsBqKmWsZdY1hyOrPpEO1XBMPF2npfclI,5126
5
+ gbra-0.1.1.dist-info/licenses/LICENSE,sha256=AJaZTTeRPlE_b0EgsutlchTPFn-JXY8Sdeg06uVmYXM,1101
6
+ gbra-0.1.1.dist-info/METADATA,sha256=4MjnjiIkxrLvDTjM8GyELbCFFGdyef01XJHeoit-dgI,5456
7
+ gbra-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ gbra-0.1.1.dist-info/top_level.txt,sha256=YS-1qm9iN_uHTzfqaocQiQ21mXfMWPLD-ZYDuCbSFQo,5
9
+ gbra-0.1.1.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