orivane 0.1.0__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.
orivane-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.3
2
+ Name: orivane
3
+ Version: 0.1.0
4
+ Summary: Orivane is a python package that makes modifing architecture of an already pretrained model easier.
5
+ Author: orven
6
+ License: MIT
7
+ Requires-Dist: torch>=2.14.1
8
+ Requires-Python: >=3.14
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Orivane
12
+
13
+ Orivane is a small Python library in alpha for making experimental changes to PyTorch models. It wraps methods with input, output, or replacement callbacks and registers patch modules with the model so they participate in device movement, training mode, checkpoints, and optimizers.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ uv add orivane
19
+ ```
20
+
21
+ ## Add a trainable adapter
22
+
23
+ Patch classes inherit from `PatchBase` and call `super().__init__()` so PyTorch can register modules assigned to the patch. By default, applying a patch freezes the model's existing parameters while leaving the patch's trainable parameters trainable.
24
+
25
+ ```python
26
+ import torch
27
+ import torch.nn as nn
28
+
29
+ from orivane import PatchBase, PatchType, inject, patch
30
+
31
+ model = nn.Linear(10, 5)
32
+
33
+ @patch(model=model)
34
+ class AdapterPatch(PatchBase):
35
+ def __init__(self):
36
+ super().__init__()
37
+ self.adapter = nn.Sequential(
38
+ nn.Linear(5, 16),
39
+ nn.GELU(),
40
+ nn.Linear(16, 5),
41
+ )
42
+
43
+ @inject(method="forward", at=PatchType.AFTER)
44
+ def add_adapter(self, output):
45
+ return output + self.adapter(output)
46
+
47
+ optimizer = torch.optim.AdamW(
48
+ (parameter for parameter in model.parameters() if parameter.requires_grad),
49
+ lr=1e-3,
50
+ )
51
+
52
+ x = torch.randn(32, 10)
53
+ target = torch.randn(32, 5)
54
+ loss = nn.MSELoss()(model(x), target)
55
+ optimizer.zero_grad()
56
+ loss.backward()
57
+ optimizer.step()
58
+
59
+ # Restore the original method and parameter trainability when finished.
60
+ AdapterPatch.unpatch()
61
+ ```
62
+
63
+ The decorated class name refers to the applied patch instance. Its added modules are registered under the model, so they are included in `model.parameters()` and `model.state_dict()`, and follow `model.to(...)` and `model.train()` calls. Patch modules are moved to the device and dtype of the model's first parameter when the patch is applied.
64
+
65
+ Use `train_only_added=False` to leave the model's existing trainability settings unchanged:
66
+
67
+ ```python
68
+ @patch(model=model, train_only_added=False)
69
+ class FineTunePatch(PatchBase):
70
+ def __init__(self):
71
+ super().__init__()
72
+ ```
73
+
74
+ Unpatching is safe to call more than once. It restores the wrapped method and the original `requires_grad` state of model parameters, and removes the patch module from the model.
75
+
76
+ ## Target a nested module
77
+
78
+ `inject(method=...)` accepts a method name on the model or a dotted path to a nested module method. For example, `"layers.0.forward"` targets the first module in a `ModuleList` named `layers`:
79
+
80
+ ```python
81
+ class BlockPatch(PatchBase):
82
+ def __init__(self):
83
+ super().__init__()
84
+ self.scale = nn.Parameter(torch.tensor(0.1))
85
+
86
+ @inject(method="layers.0.forward", at=PatchType.AFTER)
87
+ def scale_output(self, output):
88
+ return output * (1 + self.scale)
89
+ ```
90
+
91
+ Apply it with `@patch(model=model)` as in the adapter example. `BEFORE` callbacks receive the method's positional and keyword arguments and may return `(new_args, new_kwargs)`; `AFTER` callbacks receive and return the method result; `OVERWRITE` callbacks replace the method implementation.
92
+
93
+ ## Patch types
94
+
95
+ | Patch type | Behavior |
96
+ |---|---|
97
+ | `PatchType.BEFORE` | Optionally changes the arguments before the original method runs. |
98
+ | `PatchType.AFTER` | Changes the original method's result. |
99
+ | `PatchType.OVERWRITE` | Replaces the original method; only one overwrite is allowed per method. |
100
+
101
+ Patches operate on PyTorch modules and methods. They do not provide model-specific integrations, automatic layer discovery, distributed-training support, or quantization support.
102
+
103
+ ## License
104
+
105
+ See the [LICENSE](LICENSE) file for license information.
@@ -0,0 +1,95 @@
1
+ # Orivane
2
+
3
+ Orivane is a small Python library in alpha for making experimental changes to PyTorch models. It wraps methods with input, output, or replacement callbacks and registers patch modules with the model so they participate in device movement, training mode, checkpoints, and optimizers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv add orivane
9
+ ```
10
+
11
+ ## Add a trainable adapter
12
+
13
+ Patch classes inherit from `PatchBase` and call `super().__init__()` so PyTorch can register modules assigned to the patch. By default, applying a patch freezes the model's existing parameters while leaving the patch's trainable parameters trainable.
14
+
15
+ ```python
16
+ import torch
17
+ import torch.nn as nn
18
+
19
+ from orivane import PatchBase, PatchType, inject, patch
20
+
21
+ model = nn.Linear(10, 5)
22
+
23
+ @patch(model=model)
24
+ class AdapterPatch(PatchBase):
25
+ def __init__(self):
26
+ super().__init__()
27
+ self.adapter = nn.Sequential(
28
+ nn.Linear(5, 16),
29
+ nn.GELU(),
30
+ nn.Linear(16, 5),
31
+ )
32
+
33
+ @inject(method="forward", at=PatchType.AFTER)
34
+ def add_adapter(self, output):
35
+ return output + self.adapter(output)
36
+
37
+ optimizer = torch.optim.AdamW(
38
+ (parameter for parameter in model.parameters() if parameter.requires_grad),
39
+ lr=1e-3,
40
+ )
41
+
42
+ x = torch.randn(32, 10)
43
+ target = torch.randn(32, 5)
44
+ loss = nn.MSELoss()(model(x), target)
45
+ optimizer.zero_grad()
46
+ loss.backward()
47
+ optimizer.step()
48
+
49
+ # Restore the original method and parameter trainability when finished.
50
+ AdapterPatch.unpatch()
51
+ ```
52
+
53
+ The decorated class name refers to the applied patch instance. Its added modules are registered under the model, so they are included in `model.parameters()` and `model.state_dict()`, and follow `model.to(...)` and `model.train()` calls. Patch modules are moved to the device and dtype of the model's first parameter when the patch is applied.
54
+
55
+ Use `train_only_added=False` to leave the model's existing trainability settings unchanged:
56
+
57
+ ```python
58
+ @patch(model=model, train_only_added=False)
59
+ class FineTunePatch(PatchBase):
60
+ def __init__(self):
61
+ super().__init__()
62
+ ```
63
+
64
+ Unpatching is safe to call more than once. It restores the wrapped method and the original `requires_grad` state of model parameters, and removes the patch module from the model.
65
+
66
+ ## Target a nested module
67
+
68
+ `inject(method=...)` accepts a method name on the model or a dotted path to a nested module method. For example, `"layers.0.forward"` targets the first module in a `ModuleList` named `layers`:
69
+
70
+ ```python
71
+ class BlockPatch(PatchBase):
72
+ def __init__(self):
73
+ super().__init__()
74
+ self.scale = nn.Parameter(torch.tensor(0.1))
75
+
76
+ @inject(method="layers.0.forward", at=PatchType.AFTER)
77
+ def scale_output(self, output):
78
+ return output * (1 + self.scale)
79
+ ```
80
+
81
+ Apply it with `@patch(model=model)` as in the adapter example. `BEFORE` callbacks receive the method's positional and keyword arguments and may return `(new_args, new_kwargs)`; `AFTER` callbacks receive and return the method result; `OVERWRITE` callbacks replace the method implementation.
82
+
83
+ ## Patch types
84
+
85
+ | Patch type | Behavior |
86
+ |---|---|
87
+ | `PatchType.BEFORE` | Optionally changes the arguments before the original method runs. |
88
+ | `PatchType.AFTER` | Changes the original method's result. |
89
+ | `PatchType.OVERWRITE` | Replaces the original method; only one overwrite is allowed per method. |
90
+
91
+ Patches operate on PyTorch modules and methods. They do not provide model-specific integrations, automatic layer discovery, distributed-training support, or quantization support.
92
+
93
+ ## License
94
+
95
+ See the [LICENSE](LICENSE) file for license information.
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "orivane"
3
+ version = "0.1.0"
4
+ description = "Orivane is a python package that makes modifing architecture of an already pretrained model easier."
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = ["torch>=2.14.1"]
8
+
9
+ [project.license]
10
+ text = "MIT"
11
+
12
+ [[project.authors]]
13
+ name = "orven"
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.0,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [dependency-groups]
20
+ dev = []
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "orivane"
3
+ version = "0.1.0"
4
+ description = "Orivane is a python package that makes modifing architecture of an already pretrained model easier."
5
+ license = { text = "MIT" }
6
+ authors = [{ name = "orven" }]
7
+ readme = "README.md"
8
+ requires-python = ">=3.14"
9
+
10
+ dependencies = [
11
+ "torch>=2.14.1",
12
+ ]
13
+
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.12.0,<0.13.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [dependency-groups]
20
+ dev = []
@@ -0,0 +1,8 @@
1
+ from .patch import (
2
+ patch,
3
+ inject,
4
+ PatchType,
5
+ InvalidModuleException,
6
+ InvalidPatchMethodException,
7
+ PatchBase,
8
+ )
@@ -0,0 +1,5 @@
1
+ from .patch import patch
2
+ from .inject import inject
3
+ from .patch_base import PatchBase
4
+ from .patch_type import PatchType
5
+ from .patch_errors import InvalidModuleException, InvalidPatchMethodException
@@ -0,0 +1,25 @@
1
+ from functools import wraps
2
+
3
+ from .patch_errors import InvalidPatchMethodException
4
+ from .patch_type import PatchType
5
+
6
+ def inject(method: str, at: PatchType):
7
+ def decorator(func):
8
+ @wraps(func)
9
+ def wrapper(self, *args, **kwargs):
10
+ if not getattr(self, "__orivane_patched__", False):
11
+ raise InvalidPatchMethodException(f"{func.__name__} can only be used inside a class decorated with @patch.")
12
+
13
+ if getattr(self, "__orivane_model__", None) is None:
14
+ raise InvalidPatchMethodException(f"{type(self).__name__} doesn't contain an __orivane_model__.")
15
+
16
+ return func(self, *args, **kwargs)
17
+
18
+ wrapper.__orivane_injection__ = {
19
+ "method": method,
20
+ "at": at,
21
+ }
22
+
23
+ return wrapper
24
+
25
+ return decorator
@@ -0,0 +1,201 @@
1
+ from functools import wraps
2
+
3
+ from torch import nn
4
+
5
+ from .patch_base import PatchBase
6
+ from .patch_errors import InvalidModuleException, InvalidPatchMethodException
7
+ from .patch_type import PatchType
8
+
9
+ def _resolve_method(model: nn.Module, path: str):
10
+ module_path, separator, method_name = path.rpartition(".")
11
+ target = model.get_submodule(module_path) if separator else model
12
+ method = getattr(target, method_name, None)
13
+
14
+ if not callable(method):
15
+ raise InvalidPatchMethodException(
16
+ f"Method {path!r} does not exist on {type(model).__name__}."
17
+ )
18
+
19
+ return target, method_name, method
20
+
21
+
22
+ def _create_wrapper(original, injections):
23
+ before = [
24
+ callback for at, callback in injections
25
+ if at is PatchType.BEFORE
26
+ ]
27
+ after = [
28
+ callback for at, callback in injections
29
+ if at is PatchType.AFTER
30
+ ]
31
+ overwrite = [
32
+ callback for at, callback in injections
33
+ if at is PatchType.OVERWRITE
34
+ ]
35
+
36
+ if len(overwrite) > 1:
37
+ raise InvalidPatchMethodException(
38
+ "Only one OVERWRITE injection is allowed per method."
39
+ )
40
+
41
+ implementation = overwrite[0] if overwrite else original
42
+
43
+ @wraps(original)
44
+ def wrapper(*args, **kwargs):
45
+ for callback in before:
46
+ result = callback(*args, **kwargs)
47
+
48
+ if result is not None:
49
+ if (
50
+ not isinstance(result, tuple)
51
+ or len(result) != 2
52
+ or not isinstance(result[0], tuple)
53
+ or not isinstance(result[1], dict)
54
+ ):
55
+ raise TypeError(
56
+ "BEFORE injections must return None or "
57
+ "(args: tuple, kwargs: dict)."
58
+ )
59
+
60
+ args, kwargs = result
61
+
62
+ output = implementation(*args, **kwargs)
63
+
64
+ for callback in after:
65
+ output = callback(output)
66
+
67
+ return output
68
+
69
+ return wrapper
70
+
71
+
72
+ def _next_patch_name(model: nn.Module) -> str:
73
+ index = 0
74
+ while f"_orivane_patch_{index}" in model._modules:
75
+ index += 1
76
+ return f"_orivane_patch_{index}"
77
+
78
+
79
+ def patch(*, model: nn.Module, train_only_added: bool = True):
80
+ if not isinstance(model, nn.Module):
81
+ raise InvalidModuleException("'model' must inherit from torch.nn.Module.")
82
+
83
+ def decorator(cls):
84
+ if not issubclass(cls, PatchBase):
85
+ raise InvalidPatchMethodException(
86
+ f"{cls.__name__} must inherit from PatchBase."
87
+ )
88
+
89
+ cls.__orivane_patch_decorator_applied__ = True
90
+ instance = cls()
91
+ instance._set_model(model)
92
+
93
+ reference = next(model.parameters(), None)
94
+ if reference is not None:
95
+ instance.to(device=reference.device, dtype=reference.dtype)
96
+
97
+ injections = {}
98
+ for name, member in vars(cls).items():
99
+ metadata = getattr(member, "__orivane_injection__", None)
100
+ if metadata is None:
101
+ continue
102
+
103
+ method_path = metadata["method"]
104
+ at = metadata["at"]
105
+ if not isinstance(method_path, str) or not method_path:
106
+ raise InvalidPatchMethodException(
107
+ "Injected method paths must be non-empty strings."
108
+ )
109
+ if not isinstance(at, PatchType):
110
+ raise InvalidPatchMethodException(
111
+ f"Invalid patch type for method {method_path!r}."
112
+ )
113
+
114
+ injections.setdefault(method_path, []).append(
115
+ (at, getattr(instance, name))
116
+ )
117
+
118
+ restorations = []
119
+ for method_path, callbacks in injections.items():
120
+ target, method_name, original = _resolve_method(model, method_path)
121
+ had_instance_attr = method_name in target.__dict__
122
+ old_instance_attr = target.__dict__.get(method_name)
123
+ restorations.append(
124
+ (
125
+ target,
126
+ method_name,
127
+ had_instance_attr,
128
+ old_instance_attr,
129
+ _create_wrapper(original, callbacks),
130
+ )
131
+ )
132
+
133
+ patch_name = _next_patch_name(model)
134
+ instance.__orivane_patched__ = True
135
+ original_trainability = []
136
+ applied_restorations = []
137
+ registered = False
138
+
139
+ try:
140
+ model.add_module(patch_name, instance)
141
+ registered = True
142
+
143
+ if train_only_added:
144
+ patch_parameter_ids = {
145
+ id(parameter) for parameter in instance.parameters()
146
+ }
147
+ for parameter in model.parameters():
148
+ if id(parameter) not in patch_parameter_ids:
149
+ original_trainability.append(
150
+ (parameter, parameter.requires_grad)
151
+ )
152
+ parameter.requires_grad_(False)
153
+
154
+ for target, method_name, existed, old_value, wrapped in restorations:
155
+ setattr(target, method_name, wrapped)
156
+ applied_restorations.append(
157
+ (target, method_name, existed, old_value)
158
+ )
159
+ except Exception:
160
+ for target, method_name, existed, old_value in reversed(
161
+ applied_restorations
162
+ ):
163
+ if existed:
164
+ setattr(target, method_name, old_value)
165
+ else:
166
+ delattr(target, method_name)
167
+ for parameter, requires_grad in original_trainability:
168
+ parameter.requires_grad_(requires_grad)
169
+ if registered:
170
+ delattr(model, patch_name)
171
+ instance.__orivane_patched__ = False
172
+ raise
173
+
174
+ active = True
175
+
176
+ def unpatch():
177
+ nonlocal active
178
+ if not active:
179
+ return
180
+
181
+ for target, method_name, existed, old_value in reversed(
182
+ applied_restorations
183
+ ):
184
+ if existed:
185
+ setattr(target, method_name, old_value)
186
+ else:
187
+ delattr(target, method_name)
188
+
189
+ for parameter, requires_grad in original_trainability:
190
+ parameter.requires_grad_(requires_grad)
191
+
192
+ if model._modules.get(patch_name) is instance:
193
+ delattr(model, patch_name)
194
+
195
+ instance.__orivane_patched__ = False
196
+ active = False
197
+
198
+ instance.unpatch = unpatch
199
+ return instance
200
+
201
+ return decorator
@@ -0,0 +1,70 @@
1
+ from torch import nn
2
+
3
+ from .patch_errors import InvalidModuleException, InvalidPatchMethodException
4
+
5
+ class _PatchMeta(type(nn.Module)):
6
+ def __call__(cls, *args, **kwargs):
7
+ has_injections = any(
8
+ getattr(member, "__orivane_injection__", None) is not None
9
+ for member in vars(cls).values()
10
+ )
11
+ decorator_applied = cls.__dict__.get(
12
+ "__orivane_patch_decorator_applied__", False
13
+ )
14
+
15
+ if has_injections and not decorator_applied:
16
+ raise InvalidPatchMethodException(
17
+ f"{cls.__name__} contains injected methods and must be "
18
+ "decorated with @patch."
19
+ )
20
+
21
+ return super().__call__(*args, **kwargs)
22
+
23
+
24
+ class PatchBase(nn.Module, metaclass=_PatchMeta):
25
+ def __init__(self):
26
+ super().__init__()
27
+ object.__setattr__(self, "__orivane_model__", None)
28
+ self.__orivane_added_modules__ = {}
29
+ self.__orivane_patched__ = False
30
+
31
+ def _set_model(self, model: nn.Module):
32
+ object.__setattr__(self, "__orivane_model__", model)
33
+
34
+ def add_module(
35
+ self,
36
+ name: str,
37
+ module: nn.Module,
38
+ *,
39
+ trainable: bool = True,
40
+ device=None,
41
+ ):
42
+ model = self.__orivane_model__
43
+
44
+ if not isinstance(module, nn.Module):
45
+ raise InvalidModuleException(
46
+ "'module' has to inherit from torch.nn.Module."
47
+ )
48
+
49
+ if name in self._modules or hasattr(self, name):
50
+ raise ValueError(f"Patch attribute {name!r} already exists.")
51
+
52
+ if device is None and model is not None:
53
+ reference = next(model.parameters(), None)
54
+ if reference is not None:
55
+ device = reference.device
56
+
57
+ if device is not None:
58
+ module.to(device)
59
+
60
+ module.requires_grad_(trainable)
61
+ super().add_module(name, module)
62
+ self.__orivane_added_modules__[name] = module
63
+ return module
64
+
65
+ def trainable_parameters(self):
66
+ return (
67
+ parameter
68
+ for parameter in self.parameters()
69
+ if parameter.requires_grad
70
+ )
@@ -0,0 +1,7 @@
1
+
2
+
3
+ class InvalidPatchMethodException(Exception):
4
+ pass
5
+
6
+ class InvalidModuleException(Exception):
7
+ pass
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+ class PatchType(Enum):
4
+ BEFORE = 0 # Changes the input for the original function, it's output is the new input for the orignal function
5
+ AFTER = 1 # Changes the output, it's input is the output of the original function
6
+ OVERWRITE = 2 # Overwrites the whole function