orivane 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.
- orivane/__init__.py +8 -0
- orivane/patch/__init__.py +5 -0
- orivane/patch/inject.py +25 -0
- orivane/patch/patch.py +201 -0
- orivane/patch/patch_base.py +70 -0
- orivane/patch/patch_errors.py +7 -0
- orivane/patch/patch_type.py +6 -0
- orivane-0.1.0.dist-info/METADATA +105 -0
- orivane-0.1.0.dist-info/RECORD +10 -0
- orivane-0.1.0.dist-info/WHEEL +4 -0
orivane/__init__.py
ADDED
orivane/patch/inject.py
ADDED
|
@@ -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
|
orivane/patch/patch.py
ADDED
|
@@ -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,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
|
|
@@ -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,10 @@
|
|
|
1
|
+
orivane/__init__.py,sha256=NLnDfMzpR5u3-ipdtJD3QvoXhUmDCa6bp_IBlEp0cDk,136
|
|
2
|
+
orivane/patch/__init__.py,sha256=EvbMGquBUEA5RR1dRsH7EZlu3_pelMYJtbmer46HxdE,197
|
|
3
|
+
orivane/patch/inject.py,sha256=sZGQ6rquwqFyDOaKXBGVRvQ586mkqFIwNWr4IrWCD_U,819
|
|
4
|
+
orivane/patch/patch.py,sha256=tpiW8r5oVBgqNEuVDA-HVMi2rMxCEN7vWx5pJZEXZ-A,6563
|
|
5
|
+
orivane/patch/patch_base.py,sha256=62gwIUw_mCfpv467EecxlYTI4YVG1T-xbu5jQiUxaSo,2146
|
|
6
|
+
orivane/patch/patch_errors.py,sha256=SLg2OOhPR4_yzZr8jc6GlZiXuHPR9K43_sK1H5Q24qM,107
|
|
7
|
+
orivane/patch/patch_type.py,sha256=RJsI4GP-6GFM2wEwKw6rYerzxOifJG5bWEPvhQw9_fQ,297
|
|
8
|
+
orivane-0.1.0.dist-info/WHEEL,sha256=oy45bIW3ehK02fnLDUKD-FzeaR3VpJxY5Jdblkktnpw,80
|
|
9
|
+
orivane-0.1.0.dist-info/METADATA,sha256=5C_aowUJozQR1ZXvlRjSbsrNG0R-IaVKCmec5jH1UOc,3879
|
|
10
|
+
orivane-0.1.0.dist-info/RECORD,,
|