torchsystem 1.2.0__tar.gz → 1.2.2__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.
- torchsystem-1.2.2/PKG-INFO +414 -0
- torchsystem-1.2.2/README.md +399 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/pyproject.toml +1 -1
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/compiler.py +4 -5
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/services/prodcon.py +67 -8
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/services/service.py +18 -7
- torchsystem-1.2.0/PKG-INFO +0 -19
- torchsystem-1.2.0/README.md +0 -4
- {torchsystem-1.2.0 → torchsystem-1.2.2}/LICENSE +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/__init__.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/aggregate.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/depends.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/domain.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/events.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/registry.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/services/__init__.py +0 -0
- {torchsystem-1.2.0 → torchsystem-1.2.2}/torchsystem/services/pubsub.py +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: torchsystem
|
|
3
|
+
Version: 1.2.2
|
|
4
|
+
Summary: A framework for creating message-driven IA systems with PyTorch
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: mr-mapache
|
|
7
|
+
Author-email: eric.m.cardozo@mi.unc.edu.ar
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: mlregistry (>=1.2.1,<2.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# TorchSystem.
|
|
17
|
+
|
|
18
|
+
This framework will help you to create powerful and scalable systems using the PyTorch library. It is designed under the principles of domain driven design (DDD) and includes built-in message patterns and a robust dependency injection system. It enables the creation of stateless, modular service layers and robust domain models. This design facilitates better separation of concerns, testability, and scalability, making it ideal for complex IA training systems. You can find the full documentation here: [mr-mapache.github.io/torch-system/](https://mr-mapache.github.io/torch-system/)
|
|
19
|
+
|
|
20
|
+
## Table of contents:
|
|
21
|
+
|
|
22
|
+
- [Introduction](#introduction)
|
|
23
|
+
- [Features](#features)
|
|
24
|
+
- [Instalation](#instalation)
|
|
25
|
+
- [Example](#example)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
## Introduction
|
|
29
|
+
|
|
30
|
+
In domain-driven design, an aggregate is a cluster of associated objects that we treat as a unit for the purpose of data changes. It acts as a boundary around its constituent objects, encapsulating their behavior and ensuring that all changes to its state occur through well-defined entry points.
|
|
31
|
+
|
|
32
|
+
In the context of deep learning, a model not only consists of a neural network but also a set of associated objects that are necessary for the tasks it performs, such as loss functions, tokenizers, etc. This defines an aggregate.
|
|
33
|
+
|
|
34
|
+
While aggregates are in charge of data, in order to perform actions, we need to define services. Services are stateless operations that fulfill domain-specific tasks. For example, when training a neural network, the model doesn't own the data on which it is trained or how the training is performed. The training process is a stateless operation that resides outside the model and should be defined as a service.
|
|
35
|
+
|
|
36
|
+
Services may produce data, such as events, metrics, or logs, that are not their responsibility to handle. This introduces the need for a messaging system that allows services to communicate with each other.
|
|
37
|
+
|
|
38
|
+
With all this in mind, the need for a well-defined framework that defines aggregates and handles service interactions becomes evident. While it is up to the developer to define his domain, this framework provides a set of tools to facilitate it's implementation.
|
|
39
|
+
|
|
40
|
+
## Instalation
|
|
41
|
+
|
|
42
|
+
To install the framework, you can use pip:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install torchsystem
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Aggregates**: Define the structure of your domain by grouping related entities and enforcing consistency within their boundaries. They encapsulate both data and behavior, ensuring that all modifications occur through controlled operations.
|
|
51
|
+
|
|
52
|
+
- **Domain Events**: Aggregates can produce and consume domain events, which signal meaningful changes in the system or trigger actions elsewhere. Exceptions are supported to be treated as domain events, allowing them to be enqueued and handled or rised as needed. This makes it trivial to implement features like early stopping (Just enqueue an exception and raise it when needed).
|
|
53
|
+
|
|
54
|
+
- **Dependency Injection**: The framework provides a robust dependency injection system that allows you to define and inject dependencies. This enables you to define your logic in terms of interfaces and inject implementations later.
|
|
55
|
+
|
|
56
|
+
- **Registry**: The registry module allows you to treat your models as entities by providing a way to calculate locally unique hashes for them that can act as their identifier. This module also provides several other utilities to help you handle the data from your domain.
|
|
57
|
+
|
|
58
|
+
- **Compilers**: Building aggregates can be a complex process. In the context of deep learning, aggregates not only need to be built but also compiled, making compilation an integral part of the construction process. This framework provides a Compiler class to help define and manage the compilation process for your aggregates
|
|
59
|
+
|
|
60
|
+
- **Services**: Define stateless operations that fulfill domain-specific tasks using ubiquitous language.
|
|
61
|
+
|
|
62
|
+
- **Producers/Consumers**: Events produced by services can be delivered by producers to several consumers. This allows you to decouple services and define complex interactions between them.
|
|
63
|
+
|
|
64
|
+
- **Publisher/Subscriber**: Data also can be delivered with the publisher/subscriber pattern. Publishers can send data to subscribers using a topic-based system.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
Let's build a simple training system using the framework. First, we can define our domain with interfaces:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# src/domain.py
|
|
73
|
+
from typing import Any
|
|
74
|
+
from typing import Protocol
|
|
75
|
+
|
|
76
|
+
from torch import Tensor
|
|
77
|
+
from torch.nn import Module
|
|
78
|
+
from torchsystem import Events
|
|
79
|
+
|
|
80
|
+
class Model(Protocol):
|
|
81
|
+
id: Any
|
|
82
|
+
phase: str
|
|
83
|
+
epoch: int
|
|
84
|
+
events: Events # You will find out why we need this later
|
|
85
|
+
nn: Module
|
|
86
|
+
criterion: Module
|
|
87
|
+
optimizer: Module
|
|
88
|
+
|
|
89
|
+
def fit(self, *args, **kwargs) -> Any:...
|
|
90
|
+
|
|
91
|
+
def evaluate(self, *args, **kwargs) -> Any:...
|
|
92
|
+
|
|
93
|
+
class Metric(Protocol):
|
|
94
|
+
name: str
|
|
95
|
+
value: Any
|
|
96
|
+
|
|
97
|
+
class Metrics(Protocol):
|
|
98
|
+
|
|
99
|
+
def update(self, *args, **kwargs) -> None:...
|
|
100
|
+
|
|
101
|
+
def compute(self) -> Sequence[Metric]:...
|
|
102
|
+
|
|
103
|
+
class Loader(Protocol):
|
|
104
|
+
|
|
105
|
+
def __iter__(self) -> Iterator[tuple[Tensor, Tensor]]:...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Notice that we didn't define any implementation. Of course you can just implement it right away, but let's define a training service first. Service handlers and events produced in the services should be modeled using an ubiquitous language. Under this idea, the framework provides a way to invoke services or produce events using their names. This is completly optional, like everything in the library, and you can invoke services handlers and dispatch events directly.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# src/services/training.py
|
|
112
|
+
from typing import Sequence
|
|
113
|
+
from dataclasses import dataclass
|
|
114
|
+
|
|
115
|
+
from torch import inference_mode
|
|
116
|
+
from torchsystem import Depends
|
|
117
|
+
from torchsystem.services import Service
|
|
118
|
+
from torchsystem.services import Producer
|
|
119
|
+
|
|
120
|
+
from src.domain import Model
|
|
121
|
+
from src.domain import Loader
|
|
122
|
+
from src.domain import Metric, Metrics
|
|
123
|
+
|
|
124
|
+
service = Service()
|
|
125
|
+
producer = Producer()
|
|
126
|
+
|
|
127
|
+
@service.handler
|
|
128
|
+
def iterate(model: Model, loaders: Sequence[tuple[str, Loader]], metrics: Metrics):
|
|
129
|
+
for phase, loader in loaders:
|
|
130
|
+
train(model, loader, metrics) if phase == 'train' else validate(model, loader, metrics)
|
|
131
|
+
service.handle('train', model, loader, metrics) if phase == 'train' else service.handle('validate', model, loader, metrics)
|
|
132
|
+
model.epoch += 1
|
|
133
|
+
producer.produce('iterated', model, loaders)
|
|
134
|
+
|
|
135
|
+
def device() -> str:
|
|
136
|
+
raise NotImplementedError("We don't know the device yet!!!, will be injected later")
|
|
137
|
+
|
|
138
|
+
@service.handler
|
|
139
|
+
def train(model: Model, loader: Loader, metrics: Metrics, device: str = Depends(device)):
|
|
140
|
+
model.phase = 'train'
|
|
141
|
+
for batch, (input, target) in enumerate(loader, start=1):
|
|
142
|
+
input, target = input.to(device), target.to(device)
|
|
143
|
+
prediction, loss = model.fit(input, target)
|
|
144
|
+
metrics.update(batch, loss, prediction, target)
|
|
145
|
+
sequence = metrics.compute()
|
|
146
|
+
producer.produce('trained', model, loader, sequence)
|
|
147
|
+
|
|
148
|
+
@service.handler
|
|
149
|
+
def validate(model: Model, loader: Loader, metrics: Metrics, device: str = Depends(device)):
|
|
150
|
+
model.phase = 'evaluation'
|
|
151
|
+
with inference_mode():
|
|
152
|
+
for batch, (input, target) in enumerate(loader, start=1):
|
|
153
|
+
input, target = input.to(device), target.to(device)
|
|
154
|
+
prediction, loss = model.evaluate(input, target)
|
|
155
|
+
metrics.update(batch, loss, prediction, target)
|
|
156
|
+
sequence = metrics.compute()
|
|
157
|
+
producer.produce('validated', model, loader, sequence)
|
|
158
|
+
|
|
159
|
+
@producer.event
|
|
160
|
+
class Iterated:
|
|
161
|
+
model: Model
|
|
162
|
+
loaders: Sequence[tuple[str, Loader]]
|
|
163
|
+
|
|
164
|
+
@producer.event
|
|
165
|
+
class Trained:
|
|
166
|
+
model: Model
|
|
167
|
+
loader: Loader
|
|
168
|
+
metrics: Sequence[Metric]
|
|
169
|
+
|
|
170
|
+
@producer.event
|
|
171
|
+
class Validated:
|
|
172
|
+
model: Model
|
|
173
|
+
loader: Loader
|
|
174
|
+
metrics: Sequence[Metric]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
And that's it! A simple training system. Notice that it is completely decoupled from the implementation of the domain. It's only task is to orchestrate the training process and produce events from it. It doesn't provide any storage logic or data manipulation, only stateless training logic. Now you can now build a whole data storage system, logging or any other service you need around this simple service, thanks to the event system.
|
|
178
|
+
|
|
179
|
+
Let's create a simple tensorboard consumer for this service:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# src/services/tensorboard.py
|
|
183
|
+
from torchsystem import Depends
|
|
184
|
+
from torchsystem.services import Consumer
|
|
185
|
+
from torch.utils.tensorboard.writer import SummaryWriter
|
|
186
|
+
from trainsys.services.training import (
|
|
187
|
+
Trained,
|
|
188
|
+
Validated
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
consumer = Consumer()
|
|
192
|
+
|
|
193
|
+
def writer() -> SummaryWriter:
|
|
194
|
+
raise NotImplementedError("We don't want to handle the writer here!!!, will be injected later in the application layer")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@consumer.handler
|
|
198
|
+
def deliver_metrics(event: Trained | Validated, writer: SummaryWriter = Depends(writer)):
|
|
199
|
+
for metric in event.metrics:
|
|
200
|
+
writer.add_scalar(f'{event.model.id}/{metric.name}/{event.model.phase}', metric.value, event.model.epoch)
|
|
201
|
+
|
|
202
|
+
# When you pass an Union of types as annotation, the consumer will register the handler for all types in the Union
|
|
203
|
+
# automatically. This is a good way to handle events that share the same logic.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Since several consumers can consume from the same producer, you can plug any service you want to the training system. The service don't need to know who is consuming the events it produces. This is known a dependency inversion principle. You now build a whole system around this simple training service. All kind of logic can be implemented from here, from weights storage to early stopping. Let's create an early stopping service:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# src/services/earlystopping.py
|
|
210
|
+
from torchsystem.services import Subscriber
|
|
211
|
+
from torchsystem.services import Consumer
|
|
212
|
+
|
|
213
|
+
from trainsys.services.training import Metric
|
|
214
|
+
from trainsys.services.training import Trained, Validated
|
|
215
|
+
|
|
216
|
+
subscriber = Subscriber()
|
|
217
|
+
consumer = Consumer()
|
|
218
|
+
|
|
219
|
+
@consumer.handler
|
|
220
|
+
def deliver_metrics(event: Trained | Validated):
|
|
221
|
+
for metric in event.metrics:
|
|
222
|
+
try:
|
|
223
|
+
subscriber.receive(metric, metric.name)
|
|
224
|
+
except StopIteration:
|
|
225
|
+
event.model.events.enqueue(StopIteration) # Exceptions are also supported as domain events
|
|
226
|
+
# If you prefer you can create a domain event for this
|
|
227
|
+
# and enqueue it here.
|
|
228
|
+
@subscriber.subscribe('loss')
|
|
229
|
+
def on_low_loss(metric: Metric):
|
|
230
|
+
if metric.value < 0.001:
|
|
231
|
+
raise StopIteration # Built-in exception from Python
|
|
232
|
+
|
|
233
|
+
@subscriber.subscribe('accuracy')
|
|
234
|
+
def on_high_accuracy(metric: Metric):
|
|
235
|
+
if metric.value > 0.995:
|
|
236
|
+
raise StopIteration # Exceptions are a good way to propagate information backwards in the system.
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This is a simple early stopping service. It listens to the metrics produced by the training service and raises a StopIteration exception when the loss is low enough or the accuracy is high enough. This exception is enqueued in the model events and can be raised again when needed, for example within the `onepoch` hook in the aggregate. A `Publisher` could also be used to send the messages to the subscribers but it wasn't necessary in this case.
|
|
240
|
+
|
|
241
|
+
Now we are going to implement a simple classifier aggregate in order to train a neural network for image classification tasks.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from torch import argmax
|
|
245
|
+
from torch import Tensor
|
|
246
|
+
from torch.nn import Module
|
|
247
|
+
from torch.nn import Flatten
|
|
248
|
+
from torch.optim import Optimizer
|
|
249
|
+
from torchsystem.domain import Event
|
|
250
|
+
from torchsystem.domain import Aggregate
|
|
251
|
+
from torchsystem.registry import gethash
|
|
252
|
+
|
|
253
|
+
class Classifier(Aggregate):
|
|
254
|
+
def __init__(self, nn: Module, criterion: Module, optimizer: Optimizer):
|
|
255
|
+
super().__init__()
|
|
256
|
+
self.epoch = 0
|
|
257
|
+
self.nn = nn
|
|
258
|
+
self.criterion = criterion
|
|
259
|
+
self.optimizer = optimizer
|
|
260
|
+
self.flatten = Flatten()
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def id(self) -> str:
|
|
264
|
+
return gethash(self.nn) # This will return an identifier for the root
|
|
265
|
+
|
|
266
|
+
def forward(self, input: Tensor) -> tuple[Tensor, Tensor]:
|
|
267
|
+
input = self.flatten(input)
|
|
268
|
+
return self.nn(input)
|
|
269
|
+
|
|
270
|
+
def loss(self, output: Tensor, target: Tensor) -> Tensor:
|
|
271
|
+
return self.criterion(output, target)
|
|
272
|
+
|
|
273
|
+
def fit(self, input: Tensor, target: Tensor) -> tuple[Tensor, Tensor]:
|
|
274
|
+
self.optimizer.zero_grad()
|
|
275
|
+
output = self(input)
|
|
276
|
+
loss = self.loss(output, target)
|
|
277
|
+
loss.backward()
|
|
278
|
+
self.optimizer.step()
|
|
279
|
+
return argmax(output, dim=1), loss # returns classification predictions and their loss
|
|
280
|
+
|
|
281
|
+
def evaluate(self, input: Tensor, target: Tensor) -> tuple[Tensor, Tensor]:
|
|
282
|
+
output = self(input)
|
|
283
|
+
return argmax(output, dim=1), self.loss(output, target)
|
|
284
|
+
|
|
285
|
+
def onepoch(self):
|
|
286
|
+
# Hook that will be called after the epoch attribute is updated. The aggregate handle this
|
|
287
|
+
# automatically for epochs and phase.
|
|
288
|
+
self.events.commit() # This will raise the StopIteration exception when the epoch is over
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
As you see, aggregates is just a simple facade to encapsulate things you already knew. Aggregates have also the capability to handle domain events or domain exceptions.
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
class SomeDomainEvent(Event):...
|
|
295
|
+
# A simple class can represent a domain event.
|
|
296
|
+
|
|
297
|
+
class DomainException(Exception):...
|
|
298
|
+
# Exceptions are also supported as domain events.
|
|
299
|
+
# If no handler is found, they will be raised when the event queue is commited.
|
|
300
|
+
|
|
301
|
+
class DomainEventWithData(Event):
|
|
302
|
+
# They also can carry data
|
|
303
|
+
def __init__(self, data: Any):
|
|
304
|
+
self.data = data
|
|
305
|
+
|
|
306
|
+
class DomainEventWithIgnoredData(Event):
|
|
307
|
+
def __init__(self, data: Any):
|
|
308
|
+
self.data = data
|
|
309
|
+
|
|
310
|
+
class Classifier(Aggregate):
|
|
311
|
+
def __init__(self, nn: Module, criterion: Module, optimizer: Optimizer):
|
|
312
|
+
super().__init__()
|
|
313
|
+
self.events.handlers[SomeDomainEvent] = lambda: print('SomeDomainEvent handled!')
|
|
314
|
+
self.events.handlers[DomainException] = lambda: print('DomainException handled!')
|
|
315
|
+
self.events.handlers[DomainEventWithData] = lambda event: print(f'DomainEventWithData handled with data: {event.data}')
|
|
316
|
+
self.events.handlers[DomainEventWithIgnoredData] = lambda: print(f'DomainEventWithIgnoredData handled')
|
|
317
|
+
# Usually you need to define the handlers outside the aggregate and pass them in the building process. But
|
|
318
|
+
# This is an example of how you can handle complex domain logic within the aggregate.
|
|
319
|
+
...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The `Classifier` aggregate we just created can be built and compiled in a simple way. However you will find yourself in situations where you need to pick a torch backend, create and clean multiprocessing resources, pickle modules, etc., and you will need a tool to build it an compile it. I will implement a compilation pipeline, just to show you how to use the compiler:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
# src/services/compilation.py
|
|
326
|
+
from logging import getLogger
|
|
327
|
+
from torch.nn import Module
|
|
328
|
+
from torch.optim import Optimizer
|
|
329
|
+
from torchsystem import Depends
|
|
330
|
+
from torchsystem.compiler import compile
|
|
331
|
+
from torchsystem.compiler import Compiler
|
|
332
|
+
from trainsys.domain.models import Repository
|
|
333
|
+
from trainsys.ports.models import Models
|
|
334
|
+
from src.classifier import Classifier
|
|
335
|
+
|
|
336
|
+
logger = getLogger(__name__)
|
|
337
|
+
compiler = Compiler[Classifier]()
|
|
338
|
+
|
|
339
|
+
def device() -> str:...
|
|
340
|
+
|
|
341
|
+
def epoch() -> int:
|
|
342
|
+
return 10 # Just for the sake of the example
|
|
343
|
+
|
|
344
|
+
@compiler.step
|
|
345
|
+
def build_classifier(model: Module, criterion: Module, optimizer: Optimizer):
|
|
346
|
+
logger.info(f'Building Classifier')
|
|
347
|
+
return Classifier(model, criterion, optimizer)
|
|
348
|
+
|
|
349
|
+
@compiler.step
|
|
350
|
+
def move_to_device(classifier: Classifier, device = Depends(device)):
|
|
351
|
+
logger.info(f'Moving classifier to device: {device}')
|
|
352
|
+
return classifier.to(device)
|
|
353
|
+
|
|
354
|
+
@compiler.step
|
|
355
|
+
def compile_classifier(classifier: Classifier):
|
|
356
|
+
logger.info(f'Compiling classifier')
|
|
357
|
+
return compile(classifier)
|
|
358
|
+
|
|
359
|
+
@compiler.step
|
|
360
|
+
def get_current_epoch(classifier: Classifier, epoch: int = Depends(epoch)):
|
|
361
|
+
# Implement this with some database query or api call.
|
|
362
|
+
classifier.epoch = epoch
|
|
363
|
+
return classifier
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Finally, you can put all together in the application layer as follows:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
# src/main.py
|
|
370
|
+
|
|
371
|
+
from torch import cuda
|
|
372
|
+
from torch.utils.tensorboard.writer import SummaryWriter
|
|
373
|
+
|
|
374
|
+
from src.service import (
|
|
375
|
+
training,
|
|
376
|
+
tensorboard,
|
|
377
|
+
earlystopping
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
model = MLP(...)
|
|
381
|
+
criterion = CrossEntropyLoss(...)
|
|
382
|
+
optimizer = Adam(...)
|
|
383
|
+
metrics = Metrics(...)
|
|
384
|
+
loaders = [
|
|
385
|
+
('train', DataLoader(...)),
|
|
386
|
+
('validation', DataLoader(...))
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
def device():
|
|
390
|
+
return 'cuda' if cuda.is_available() else 'cpu'
|
|
391
|
+
|
|
392
|
+
def summary_writer():
|
|
393
|
+
writter = SummaryWriter(...)
|
|
394
|
+
yield writter
|
|
395
|
+
writter.close()
|
|
396
|
+
|
|
397
|
+
training.service.dependency_overrides[training.device] = device
|
|
398
|
+
compilation.compiler.dependency_overrides[compilation.device] = device
|
|
399
|
+
tensorboard.consumer.dependency_overrides[tensorboard.writer] = summary_writer
|
|
400
|
+
|
|
401
|
+
...
|
|
402
|
+
|
|
403
|
+
classifier = compilation.compiler.compile(model, criterion, optimizer)
|
|
404
|
+
|
|
405
|
+
training.service.handle('iterate', classifier, loaders, metrics)
|
|
406
|
+
|
|
407
|
+
...
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
This is a simple example of how to build a training system using the framework. Since services can be called by their name, you can easily write a REST API with CQS or a CLI interfaces for your training system. The possibilities are endless.
|
|
411
|
+
|
|
412
|
+
## License
|
|
413
|
+
|
|
414
|
+
This project is licensed under the MIT License - Use it as you wish in your projects.
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# TorchSystem.
|
|
2
|
+
|
|
3
|
+
This framework will help you to create powerful and scalable systems using the PyTorch library. It is designed under the principles of domain driven design (DDD) and includes built-in message patterns and a robust dependency injection system. It enables the creation of stateless, modular service layers and robust domain models. This design facilitates better separation of concerns, testability, and scalability, making it ideal for complex IA training systems. You can find the full documentation here: [mr-mapache.github.io/torch-system/](https://mr-mapache.github.io/torch-system/)
|
|
4
|
+
|
|
5
|
+
## Table of contents:
|
|
6
|
+
|
|
7
|
+
- [Introduction](#introduction)
|
|
8
|
+
- [Features](#features)
|
|
9
|
+
- [Instalation](#instalation)
|
|
10
|
+
- [Example](#example)
|
|
11
|
+
- [License](#license)
|
|
12
|
+
|
|
13
|
+
## Introduction
|
|
14
|
+
|
|
15
|
+
In domain-driven design, an aggregate is a cluster of associated objects that we treat as a unit for the purpose of data changes. It acts as a boundary around its constituent objects, encapsulating their behavior and ensuring that all changes to its state occur through well-defined entry points.
|
|
16
|
+
|
|
17
|
+
In the context of deep learning, a model not only consists of a neural network but also a set of associated objects that are necessary for the tasks it performs, such as loss functions, tokenizers, etc. This defines an aggregate.
|
|
18
|
+
|
|
19
|
+
While aggregates are in charge of data, in order to perform actions, we need to define services. Services are stateless operations that fulfill domain-specific tasks. For example, when training a neural network, the model doesn't own the data on which it is trained or how the training is performed. The training process is a stateless operation that resides outside the model and should be defined as a service.
|
|
20
|
+
|
|
21
|
+
Services may produce data, such as events, metrics, or logs, that are not their responsibility to handle. This introduces the need for a messaging system that allows services to communicate with each other.
|
|
22
|
+
|
|
23
|
+
With all this in mind, the need for a well-defined framework that defines aggregates and handles service interactions becomes evident. While it is up to the developer to define his domain, this framework provides a set of tools to facilitate it's implementation.
|
|
24
|
+
|
|
25
|
+
## Instalation
|
|
26
|
+
|
|
27
|
+
To install the framework, you can use pip:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install torchsystem
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Aggregates**: Define the structure of your domain by grouping related entities and enforcing consistency within their boundaries. They encapsulate both data and behavior, ensuring that all modifications occur through controlled operations.
|
|
36
|
+
|
|
37
|
+
- **Domain Events**: Aggregates can produce and consume domain events, which signal meaningful changes in the system or trigger actions elsewhere. Exceptions are supported to be treated as domain events, allowing them to be enqueued and handled or rised as needed. This makes it trivial to implement features like early stopping (Just enqueue an exception and raise it when needed).
|
|
38
|
+
|
|
39
|
+
- **Dependency Injection**: The framework provides a robust dependency injection system that allows you to define and inject dependencies. This enables you to define your logic in terms of interfaces and inject implementations later.
|
|
40
|
+
|
|
41
|
+
- **Registry**: The registry module allows you to treat your models as entities by providing a way to calculate locally unique hashes for them that can act as their identifier. This module also provides several other utilities to help you handle the data from your domain.
|
|
42
|
+
|
|
43
|
+
- **Compilers**: Building aggregates can be a complex process. In the context of deep learning, aggregates not only need to be built but also compiled, making compilation an integral part of the construction process. This framework provides a Compiler class to help define and manage the compilation process for your aggregates
|
|
44
|
+
|
|
45
|
+
- **Services**: Define stateless operations that fulfill domain-specific tasks using ubiquitous language.
|
|
46
|
+
|
|
47
|
+
- **Producers/Consumers**: Events produced by services can be delivered by producers to several consumers. This allows you to decouple services and define complex interactions between them.
|
|
48
|
+
|
|
49
|
+
- **Publisher/Subscriber**: Data also can be delivered with the publisher/subscriber pattern. Publishers can send data to subscribers using a topic-based system.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
Let's build a simple training system using the framework. First, we can define our domain with interfaces:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# src/domain.py
|
|
58
|
+
from typing import Any
|
|
59
|
+
from typing import Protocol
|
|
60
|
+
|
|
61
|
+
from torch import Tensor
|
|
62
|
+
from torch.nn import Module
|
|
63
|
+
from torchsystem import Events
|
|
64
|
+
|
|
65
|
+
class Model(Protocol):
|
|
66
|
+
id: Any
|
|
67
|
+
phase: str
|
|
68
|
+
epoch: int
|
|
69
|
+
events: Events # You will find out why we need this later
|
|
70
|
+
nn: Module
|
|
71
|
+
criterion: Module
|
|
72
|
+
optimizer: Module
|
|
73
|
+
|
|
74
|
+
def fit(self, *args, **kwargs) -> Any:...
|
|
75
|
+
|
|
76
|
+
def evaluate(self, *args, **kwargs) -> Any:...
|
|
77
|
+
|
|
78
|
+
class Metric(Protocol):
|
|
79
|
+
name: str
|
|
80
|
+
value: Any
|
|
81
|
+
|
|
82
|
+
class Metrics(Protocol):
|
|
83
|
+
|
|
84
|
+
def update(self, *args, **kwargs) -> None:...
|
|
85
|
+
|
|
86
|
+
def compute(self) -> Sequence[Metric]:...
|
|
87
|
+
|
|
88
|
+
class Loader(Protocol):
|
|
89
|
+
|
|
90
|
+
def __iter__(self) -> Iterator[tuple[Tensor, Tensor]]:...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Notice that we didn't define any implementation. Of course you can just implement it right away, but let's define a training service first. Service handlers and events produced in the services should be modeled using an ubiquitous language. Under this idea, the framework provides a way to invoke services or produce events using their names. This is completly optional, like everything in the library, and you can invoke services handlers and dispatch events directly.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# src/services/training.py
|
|
97
|
+
from typing import Sequence
|
|
98
|
+
from dataclasses import dataclass
|
|
99
|
+
|
|
100
|
+
from torch import inference_mode
|
|
101
|
+
from torchsystem import Depends
|
|
102
|
+
from torchsystem.services import Service
|
|
103
|
+
from torchsystem.services import Producer
|
|
104
|
+
|
|
105
|
+
from src.domain import Model
|
|
106
|
+
from src.domain import Loader
|
|
107
|
+
from src.domain import Metric, Metrics
|
|
108
|
+
|
|
109
|
+
service = Service()
|
|
110
|
+
producer = Producer()
|
|
111
|
+
|
|
112
|
+
@service.handler
|
|
113
|
+
def iterate(model: Model, loaders: Sequence[tuple[str, Loader]], metrics: Metrics):
|
|
114
|
+
for phase, loader in loaders:
|
|
115
|
+
train(model, loader, metrics) if phase == 'train' else validate(model, loader, metrics)
|
|
116
|
+
service.handle('train', model, loader, metrics) if phase == 'train' else service.handle('validate', model, loader, metrics)
|
|
117
|
+
model.epoch += 1
|
|
118
|
+
producer.produce('iterated', model, loaders)
|
|
119
|
+
|
|
120
|
+
def device() -> str:
|
|
121
|
+
raise NotImplementedError("We don't know the device yet!!!, will be injected later")
|
|
122
|
+
|
|
123
|
+
@service.handler
|
|
124
|
+
def train(model: Model, loader: Loader, metrics: Metrics, device: str = Depends(device)):
|
|
125
|
+
model.phase = 'train'
|
|
126
|
+
for batch, (input, target) in enumerate(loader, start=1):
|
|
127
|
+
input, target = input.to(device), target.to(device)
|
|
128
|
+
prediction, loss = model.fit(input, target)
|
|
129
|
+
metrics.update(batch, loss, prediction, target)
|
|
130
|
+
sequence = metrics.compute()
|
|
131
|
+
producer.produce('trained', model, loader, sequence)
|
|
132
|
+
|
|
133
|
+
@service.handler
|
|
134
|
+
def validate(model: Model, loader: Loader, metrics: Metrics, device: str = Depends(device)):
|
|
135
|
+
model.phase = 'evaluation'
|
|
136
|
+
with inference_mode():
|
|
137
|
+
for batch, (input, target) in enumerate(loader, start=1):
|
|
138
|
+
input, target = input.to(device), target.to(device)
|
|
139
|
+
prediction, loss = model.evaluate(input, target)
|
|
140
|
+
metrics.update(batch, loss, prediction, target)
|
|
141
|
+
sequence = metrics.compute()
|
|
142
|
+
producer.produce('validated', model, loader, sequence)
|
|
143
|
+
|
|
144
|
+
@producer.event
|
|
145
|
+
class Iterated:
|
|
146
|
+
model: Model
|
|
147
|
+
loaders: Sequence[tuple[str, Loader]]
|
|
148
|
+
|
|
149
|
+
@producer.event
|
|
150
|
+
class Trained:
|
|
151
|
+
model: Model
|
|
152
|
+
loader: Loader
|
|
153
|
+
metrics: Sequence[Metric]
|
|
154
|
+
|
|
155
|
+
@producer.event
|
|
156
|
+
class Validated:
|
|
157
|
+
model: Model
|
|
158
|
+
loader: Loader
|
|
159
|
+
metrics: Sequence[Metric]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
And that's it! A simple training system. Notice that it is completely decoupled from the implementation of the domain. It's only task is to orchestrate the training process and produce events from it. It doesn't provide any storage logic or data manipulation, only stateless training logic. Now you can now build a whole data storage system, logging or any other service you need around this simple service, thanks to the event system.
|
|
163
|
+
|
|
164
|
+
Let's create a simple tensorboard consumer for this service:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# src/services/tensorboard.py
|
|
168
|
+
from torchsystem import Depends
|
|
169
|
+
from torchsystem.services import Consumer
|
|
170
|
+
from torch.utils.tensorboard.writer import SummaryWriter
|
|
171
|
+
from trainsys.services.training import (
|
|
172
|
+
Trained,
|
|
173
|
+
Validated
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
consumer = Consumer()
|
|
177
|
+
|
|
178
|
+
def writer() -> SummaryWriter:
|
|
179
|
+
raise NotImplementedError("We don't want to handle the writer here!!!, will be injected later in the application layer")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@consumer.handler
|
|
183
|
+
def deliver_metrics(event: Trained | Validated, writer: SummaryWriter = Depends(writer)):
|
|
184
|
+
for metric in event.metrics:
|
|
185
|
+
writer.add_scalar(f'{event.model.id}/{metric.name}/{event.model.phase}', metric.value, event.model.epoch)
|
|
186
|
+
|
|
187
|
+
# When you pass an Union of types as annotation, the consumer will register the handler for all types in the Union
|
|
188
|
+
# automatically. This is a good way to handle events that share the same logic.
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Since several consumers can consume from the same producer, you can plug any service you want to the training system. The service don't need to know who is consuming the events it produces. This is known a dependency inversion principle. You now build a whole system around this simple training service. All kind of logic can be implemented from here, from weights storage to early stopping. Let's create an early stopping service:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# src/services/earlystopping.py
|
|
195
|
+
from torchsystem.services import Subscriber
|
|
196
|
+
from torchsystem.services import Consumer
|
|
197
|
+
|
|
198
|
+
from trainsys.services.training import Metric
|
|
199
|
+
from trainsys.services.training import Trained, Validated
|
|
200
|
+
|
|
201
|
+
subscriber = Subscriber()
|
|
202
|
+
consumer = Consumer()
|
|
203
|
+
|
|
204
|
+
@consumer.handler
|
|
205
|
+
def deliver_metrics(event: Trained | Validated):
|
|
206
|
+
for metric in event.metrics:
|
|
207
|
+
try:
|
|
208
|
+
subscriber.receive(metric, metric.name)
|
|
209
|
+
except StopIteration:
|
|
210
|
+
event.model.events.enqueue(StopIteration) # Exceptions are also supported as domain events
|
|
211
|
+
# If you prefer you can create a domain event for this
|
|
212
|
+
# and enqueue it here.
|
|
213
|
+
@subscriber.subscribe('loss')
|
|
214
|
+
def on_low_loss(metric: Metric):
|
|
215
|
+
if metric.value < 0.001:
|
|
216
|
+
raise StopIteration # Built-in exception from Python
|
|
217
|
+
|
|
218
|
+
@subscriber.subscribe('accuracy')
|
|
219
|
+
def on_high_accuracy(metric: Metric):
|
|
220
|
+
if metric.value > 0.995:
|
|
221
|
+
raise StopIteration # Exceptions are a good way to propagate information backwards in the system.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This is a simple early stopping service. It listens to the metrics produced by the training service and raises a StopIteration exception when the loss is low enough or the accuracy is high enough. This exception is enqueued in the model events and can be raised again when needed, for example within the `onepoch` hook in the aggregate. A `Publisher` could also be used to send the messages to the subscribers but it wasn't necessary in this case.
|
|
225
|
+
|
|
226
|
+
Now we are going to implement a simple classifier aggregate in order to train a neural network for image classification tasks.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from torch import argmax
|
|
230
|
+
from torch import Tensor
|
|
231
|
+
from torch.nn import Module
|
|
232
|
+
from torch.nn import Flatten
|
|
233
|
+
from torch.optim import Optimizer
|
|
234
|
+
from torchsystem.domain import Event
|
|
235
|
+
from torchsystem.domain import Aggregate
|
|
236
|
+
from torchsystem.registry import gethash
|
|
237
|
+
|
|
238
|
+
class Classifier(Aggregate):
|
|
239
|
+
def __init__(self, nn: Module, criterion: Module, optimizer: Optimizer):
|
|
240
|
+
super().__init__()
|
|
241
|
+
self.epoch = 0
|
|
242
|
+
self.nn = nn
|
|
243
|
+
self.criterion = criterion
|
|
244
|
+
self.optimizer = optimizer
|
|
245
|
+
self.flatten = Flatten()
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def id(self) -> str:
|
|
249
|
+
return gethash(self.nn) # This will return an identifier for the root
|
|
250
|
+
|
|
251
|
+
def forward(self, input: Tensor) -> tuple[Tensor, Tensor]:
|
|
252
|
+
input = self.flatten(input)
|
|
253
|
+
return self.nn(input)
|
|
254
|
+
|
|
255
|
+
def loss(self, output: Tensor, target: Tensor) -> Tensor:
|
|
256
|
+
return self.criterion(output, target)
|
|
257
|
+
|
|
258
|
+
def fit(self, input: Tensor, target: Tensor) -> tuple[Tensor, Tensor]:
|
|
259
|
+
self.optimizer.zero_grad()
|
|
260
|
+
output = self(input)
|
|
261
|
+
loss = self.loss(output, target)
|
|
262
|
+
loss.backward()
|
|
263
|
+
self.optimizer.step()
|
|
264
|
+
return argmax(output, dim=1), loss # returns classification predictions and their loss
|
|
265
|
+
|
|
266
|
+
def evaluate(self, input: Tensor, target: Tensor) -> tuple[Tensor, Tensor]:
|
|
267
|
+
output = self(input)
|
|
268
|
+
return argmax(output, dim=1), self.loss(output, target)
|
|
269
|
+
|
|
270
|
+
def onepoch(self):
|
|
271
|
+
# Hook that will be called after the epoch attribute is updated. The aggregate handle this
|
|
272
|
+
# automatically for epochs and phase.
|
|
273
|
+
self.events.commit() # This will raise the StopIteration exception when the epoch is over
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
As you see, aggregates is just a simple facade to encapsulate things you already knew. Aggregates have also the capability to handle domain events or domain exceptions.
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
class SomeDomainEvent(Event):...
|
|
280
|
+
# A simple class can represent a domain event.
|
|
281
|
+
|
|
282
|
+
class DomainException(Exception):...
|
|
283
|
+
# Exceptions are also supported as domain events.
|
|
284
|
+
# If no handler is found, they will be raised when the event queue is commited.
|
|
285
|
+
|
|
286
|
+
class DomainEventWithData(Event):
|
|
287
|
+
# They also can carry data
|
|
288
|
+
def __init__(self, data: Any):
|
|
289
|
+
self.data = data
|
|
290
|
+
|
|
291
|
+
class DomainEventWithIgnoredData(Event):
|
|
292
|
+
def __init__(self, data: Any):
|
|
293
|
+
self.data = data
|
|
294
|
+
|
|
295
|
+
class Classifier(Aggregate):
|
|
296
|
+
def __init__(self, nn: Module, criterion: Module, optimizer: Optimizer):
|
|
297
|
+
super().__init__()
|
|
298
|
+
self.events.handlers[SomeDomainEvent] = lambda: print('SomeDomainEvent handled!')
|
|
299
|
+
self.events.handlers[DomainException] = lambda: print('DomainException handled!')
|
|
300
|
+
self.events.handlers[DomainEventWithData] = lambda event: print(f'DomainEventWithData handled with data: {event.data}')
|
|
301
|
+
self.events.handlers[DomainEventWithIgnoredData] = lambda: print(f'DomainEventWithIgnoredData handled')
|
|
302
|
+
# Usually you need to define the handlers outside the aggregate and pass them in the building process. But
|
|
303
|
+
# This is an example of how you can handle complex domain logic within the aggregate.
|
|
304
|
+
...
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The `Classifier` aggregate we just created can be built and compiled in a simple way. However you will find yourself in situations where you need to pick a torch backend, create and clean multiprocessing resources, pickle modules, etc., and you will need a tool to build it an compile it. I will implement a compilation pipeline, just to show you how to use the compiler:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
# src/services/compilation.py
|
|
311
|
+
from logging import getLogger
|
|
312
|
+
from torch.nn import Module
|
|
313
|
+
from torch.optim import Optimizer
|
|
314
|
+
from torchsystem import Depends
|
|
315
|
+
from torchsystem.compiler import compile
|
|
316
|
+
from torchsystem.compiler import Compiler
|
|
317
|
+
from trainsys.domain.models import Repository
|
|
318
|
+
from trainsys.ports.models import Models
|
|
319
|
+
from src.classifier import Classifier
|
|
320
|
+
|
|
321
|
+
logger = getLogger(__name__)
|
|
322
|
+
compiler = Compiler[Classifier]()
|
|
323
|
+
|
|
324
|
+
def device() -> str:...
|
|
325
|
+
|
|
326
|
+
def epoch() -> int:
|
|
327
|
+
return 10 # Just for the sake of the example
|
|
328
|
+
|
|
329
|
+
@compiler.step
|
|
330
|
+
def build_classifier(model: Module, criterion: Module, optimizer: Optimizer):
|
|
331
|
+
logger.info(f'Building Classifier')
|
|
332
|
+
return Classifier(model, criterion, optimizer)
|
|
333
|
+
|
|
334
|
+
@compiler.step
|
|
335
|
+
def move_to_device(classifier: Classifier, device = Depends(device)):
|
|
336
|
+
logger.info(f'Moving classifier to device: {device}')
|
|
337
|
+
return classifier.to(device)
|
|
338
|
+
|
|
339
|
+
@compiler.step
|
|
340
|
+
def compile_classifier(classifier: Classifier):
|
|
341
|
+
logger.info(f'Compiling classifier')
|
|
342
|
+
return compile(classifier)
|
|
343
|
+
|
|
344
|
+
@compiler.step
|
|
345
|
+
def get_current_epoch(classifier: Classifier, epoch: int = Depends(epoch)):
|
|
346
|
+
# Implement this with some database query or api call.
|
|
347
|
+
classifier.epoch = epoch
|
|
348
|
+
return classifier
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Finally, you can put all together in the application layer as follows:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
# src/main.py
|
|
355
|
+
|
|
356
|
+
from torch import cuda
|
|
357
|
+
from torch.utils.tensorboard.writer import SummaryWriter
|
|
358
|
+
|
|
359
|
+
from src.service import (
|
|
360
|
+
training,
|
|
361
|
+
tensorboard,
|
|
362
|
+
earlystopping
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
model = MLP(...)
|
|
366
|
+
criterion = CrossEntropyLoss(...)
|
|
367
|
+
optimizer = Adam(...)
|
|
368
|
+
metrics = Metrics(...)
|
|
369
|
+
loaders = [
|
|
370
|
+
('train', DataLoader(...)),
|
|
371
|
+
('validation', DataLoader(...))
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
def device():
|
|
375
|
+
return 'cuda' if cuda.is_available() else 'cpu'
|
|
376
|
+
|
|
377
|
+
def summary_writer():
|
|
378
|
+
writter = SummaryWriter(...)
|
|
379
|
+
yield writter
|
|
380
|
+
writter.close()
|
|
381
|
+
|
|
382
|
+
training.service.dependency_overrides[training.device] = device
|
|
383
|
+
compilation.compiler.dependency_overrides[compilation.device] = device
|
|
384
|
+
tensorboard.consumer.dependency_overrides[tensorboard.writer] = summary_writer
|
|
385
|
+
|
|
386
|
+
...
|
|
387
|
+
|
|
388
|
+
classifier = compilation.compiler.compile(model, criterion, optimizer)
|
|
389
|
+
|
|
390
|
+
training.service.handle('iterate', classifier, loaders, metrics)
|
|
391
|
+
|
|
392
|
+
...
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
This is a simple example of how to build a training system using the framework. Since services can be called by their name, you can easily write a REST API with CQS or a CLI interfaces for your training system. The possibilities are endless.
|
|
396
|
+
|
|
397
|
+
## License
|
|
398
|
+
|
|
399
|
+
This project is licensed under the MIT License - Use it as you wish in your projects.
|
|
@@ -8,15 +8,14 @@ from torchsystem.depends import Provider
|
|
|
8
8
|
|
|
9
9
|
class Compiler[T]:
|
|
10
10
|
"""
|
|
11
|
-
AGGREGATES
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Sometimes AGGREGATES may require complex initialization and be built from multiple sources, like database
|
|
12
|
+
queries, API calls, or other external resources are required to build it. Each one with it's own lifecycle
|
|
13
|
+
that may require some initialization method or cleanup after they are no longer needed.
|
|
14
14
|
|
|
15
15
|
In the context of neural networks, AGGREGATES not only should be built but also compiled. Compilation
|
|
16
16
|
is the process of converting a high-level neural network model into a low-level representation that can
|
|
17
17
|
be executed on a specific hardware platform, and can be seen as an integral part of the process of building
|
|
18
|
-
the AGGREGATE. This process get even more complex when performing distributed training
|
|
19
|
-
a dedicated pipeline is required to build and compile AGGREGATES.
|
|
18
|
+
the AGGREGATE. This process get even more complex when performing distributed training.
|
|
20
19
|
|
|
21
20
|
A `Compiler` is a class that compiles a pipeline of functions to be executed in sequence in order
|
|
22
21
|
to build an a low-level representation of the AGGREGATE. Since some compilation steps sometimes
|
|
@@ -2,7 +2,9 @@ from re import sub
|
|
|
2
2
|
from typing import Callable
|
|
3
3
|
from typing import Any
|
|
4
4
|
from typing import Union
|
|
5
|
+
from typing import overload
|
|
5
6
|
from inspect import signature
|
|
7
|
+
from dataclasses import dataclass
|
|
6
8
|
|
|
7
9
|
from torchsystem.depends import inject, Provider
|
|
8
10
|
from torchsystem.depends import Depends as Depends
|
|
@@ -18,6 +20,10 @@ class Consumer:
|
|
|
18
20
|
for deciding which handlers to invoke based on the type of the message it consumes. The message type should
|
|
19
21
|
describe the ocurrence in past tense as DDD suggests when modeling events.
|
|
20
22
|
|
|
23
|
+
The EVENTS should be modeled with UBIQUITOUS LANGUAGE. This means that the names of the events should reflect
|
|
24
|
+
the domain ocurrences that the consumer is responsible for. Keep this in mind when naming the events that will
|
|
25
|
+
be consumed by the consumer. The consumer maps it's handlers to keys generated by the EVENT's name.
|
|
26
|
+
|
|
21
27
|
Methods:
|
|
22
28
|
register:
|
|
23
29
|
Registers a message type and its corresponding handler function.
|
|
@@ -61,8 +67,8 @@ class Consumer:
|
|
|
61
67
|
self,
|
|
62
68
|
name: str = None,
|
|
63
69
|
*,
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
provider: Provider = None,
|
|
71
|
+
generator: Callable[[str], str] = lambda name: sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
|
|
66
72
|
):
|
|
67
73
|
self.name = name
|
|
68
74
|
self.handlers = dict[str, list[Callable[[Any], None]]]()
|
|
@@ -161,15 +167,13 @@ class Producer:
|
|
|
161
167
|
from torchsystem.services import Consumer
|
|
162
168
|
from torchsystem.services import Producer
|
|
163
169
|
|
|
164
|
-
class Event:...
|
|
165
|
-
|
|
166
170
|
@dataclass
|
|
167
|
-
class ModelTrained
|
|
171
|
+
class ModelTrained:
|
|
168
172
|
model: Callable
|
|
169
173
|
metrics: Sequence
|
|
170
174
|
|
|
171
175
|
@dataclass
|
|
172
|
-
class ModelEvaluated
|
|
176
|
+
class ModelEvaluated:
|
|
173
177
|
model: Callable
|
|
174
178
|
metrics: Sequence
|
|
175
179
|
|
|
@@ -178,10 +182,25 @@ class Producer:
|
|
|
178
182
|
producer = Producer()
|
|
179
183
|
producer.register(consumer)
|
|
180
184
|
producer.dispatch(ModelTrained(model, [{'name': 'loss', 'value': 0.1}, {'name': 'accuracy', 'value': 0.9}]))
|
|
185
|
+
|
|
186
|
+
# Events can also be produced and dispatched by the produce method.
|
|
187
|
+
|
|
188
|
+
@producer.event
|
|
189
|
+
class ModelIterated:
|
|
190
|
+
model: Callable
|
|
191
|
+
metrics: Sequence
|
|
192
|
+
|
|
193
|
+
producer.produce('model-iterated', model, [{'name': 'loss', 'value': 0.1}, {'name': 'accuracy', 'value': 0.9}])
|
|
181
194
|
```
|
|
182
195
|
"""
|
|
183
|
-
def __init__(
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
generator: Callable[[str], str] = lambda name: sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
|
|
200
|
+
):
|
|
184
201
|
self.consumers = list[Consumer]()
|
|
202
|
+
self.types = dict[str, str]()
|
|
203
|
+
self.generator = generator
|
|
185
204
|
|
|
186
205
|
def register(self, *consumers: Consumer):
|
|
187
206
|
"""
|
|
@@ -200,4 +219,44 @@ class Producer:
|
|
|
200
219
|
message (Any): The event to dispatch.
|
|
201
220
|
"""
|
|
202
221
|
for consumer in self.consumers:
|
|
203
|
-
consumer.consume(message)
|
|
222
|
+
consumer.consume(message)
|
|
223
|
+
|
|
224
|
+
def event(self, cls: type):
|
|
225
|
+
"""
|
|
226
|
+
Decorator for registering an event type. The event type is registered with the name of the class as the key.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
cls (type): The event class to be registered.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
type: The event class.
|
|
233
|
+
"""
|
|
234
|
+
key = self.generator(cls.__name__)
|
|
235
|
+
self.types[key] = dataclass(cls)
|
|
236
|
+
return dataclass(cls)
|
|
237
|
+
|
|
238
|
+
@overload
|
|
239
|
+
def produce(self, event_type: str, *args, **kwargs):
|
|
240
|
+
...
|
|
241
|
+
|
|
242
|
+
@overload
|
|
243
|
+
def produce(self, event_type: type, *args, **kwargs):
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
def produce(self, event_type: str | type, *args, **kwargs):
|
|
247
|
+
"""
|
|
248
|
+
Produces an event by instantiating the event class and dispatching it to all registered consumers.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
event_type (str | type): The event type to produce.
|
|
252
|
+
*args: The event class arguments.
|
|
253
|
+
**kwargs: The event class keyword arguments.
|
|
254
|
+
"""
|
|
255
|
+
if isinstance(event_type, str):
|
|
256
|
+
event_type = self.types.get(event_type, None)
|
|
257
|
+
if event_type is None:
|
|
258
|
+
raise ValueError(f'Event of type {event_type} is not registered.')
|
|
259
|
+
self.dispatch(event_type(*args, **kwargs))
|
|
260
|
+
|
|
261
|
+
elif isinstance(event_type, type):
|
|
262
|
+
self.dispatch(event_type(*args, **kwargs))
|
|
@@ -13,6 +13,17 @@ class Service:
|
|
|
13
13
|
The `Service` serves as an entry point for the service layer and provides a simple way to
|
|
14
14
|
build stateless logic for executing domain operations.
|
|
15
15
|
|
|
16
|
+
A SERVICE should be modeled with UBIQUITOUS LANGUAGE. This means that the names of handler
|
|
17
|
+
functions should reflect the domain operations that the service is responsible for. Keep
|
|
18
|
+
this in mind when naming the functions that will be registered as handlers, since the `Service`
|
|
19
|
+
class provides a method to call the handlers by their registered name. This is useful for example
|
|
20
|
+
when building REST APIs with Command Query Segregation (CQS) and you want to invoke a handler based
|
|
21
|
+
on the action they perfom (aka. The handler's name).
|
|
22
|
+
|
|
23
|
+
A naming generator can be provided to the `Service` constructor in order to customize the function names
|
|
24
|
+
to the ubiquitous language of the domain. The default generator transforms the function name from snake_case
|
|
25
|
+
to kebab-case.
|
|
26
|
+
|
|
16
27
|
Methods:
|
|
17
28
|
register:
|
|
18
29
|
Registers a handler for a specific command or query type. Handles nested or generic annotations.
|
|
@@ -46,8 +57,8 @@ class Service:
|
|
|
46
57
|
self,
|
|
47
58
|
name: str = None,
|
|
48
59
|
*,
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
provider: Provider = None,
|
|
61
|
+
generator: Callable[[str], str] = lambda name: sub(r'_', '-', name)
|
|
51
62
|
):
|
|
52
63
|
self.name = name
|
|
53
64
|
self.handlers = dict[str, Callable[..., Any]]()
|
|
@@ -57,7 +68,7 @@ class Service:
|
|
|
57
68
|
@property
|
|
58
69
|
def dependency_overrides(self) -> dict:
|
|
59
70
|
"""
|
|
60
|
-
|
|
71
|
+
An entry point for overriding the dependencies for the service. This is useful for late binding,
|
|
61
72
|
testing and changing the behavior of the service in runtime.
|
|
62
73
|
|
|
63
74
|
Returns:
|
|
@@ -110,16 +121,16 @@ class Service:
|
|
|
110
121
|
service = Service()
|
|
111
122
|
|
|
112
123
|
@service.handler
|
|
113
|
-
def
|
|
124
|
+
def train_model(model: Model, data: DataLoader, device: str = Depends(device)):
|
|
114
125
|
# Your training logic here
|
|
115
126
|
...
|
|
116
127
|
|
|
117
128
|
model = Model()
|
|
118
129
|
data = Data()
|
|
119
130
|
|
|
120
|
-
service.handle('train', model, data) # train(model, data) will also work
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
service.handle('train-model', model, data) # train(model, data) will also work
|
|
132
|
+
# but this is usefull when building REST APIs
|
|
133
|
+
# with Command Query Segregation (CQS)
|
|
123
134
|
```
|
|
124
135
|
"""
|
|
125
136
|
handler = self.handlers.get(action, None)
|
torchsystem-1.2.0/PKG-INFO
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: torchsystem
|
|
3
|
-
Version: 1.2.0
|
|
4
|
-
Summary: A framework for creating message-driven IA systems with PyTorch
|
|
5
|
-
License: MIT
|
|
6
|
-
Author: mr-mapache
|
|
7
|
-
Author-email: eric.m.cardozo@mi.unc.edu.ar
|
|
8
|
-
Requires-Python: >=3.12,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
-
Requires-Dist: mlregistry (>=1.2.1,<2.0.0)
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
|
-
# TorchSystem
|
|
17
|
-
An IA training system based on event driven programming, unit of work pattern, pubsub and domain driven desing.
|
|
18
|
-
|
|
19
|
-
Rebuilding!!! stay tuned.
|
torchsystem-1.2.0/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|