ato 2.0.0__tar.gz → 2.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.
Potentially problematic release.
This version of ato might be problematic. Click here for more details.
- {ato-2.0.0 → ato-2.1.0}/PKG-INFO +308 -511
- ato-2.1.0/README.md +944 -0
- ato-2.1.0/ato/__init__.py +1 -0
- {ato-2.0.0 → ato-2.1.0}/ato/scope.py +1 -0
- {ato-2.0.0 → ato-2.1.0}/ato.egg-info/PKG-INFO +308 -511
- {ato-2.0.0 → ato-2.1.0}/pyproject.toml +3 -3
- ato-2.0.0/README.md +0 -1147
- ato-2.0.0/ato/__init__.py +0 -1
- {ato-2.0.0 → ato-2.1.0}/LICENSE +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/adict.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/db_routers/__init__.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/db_routers/sql/__init__.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/db_routers/sql/manager.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/db_routers/sql/schema.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/hyperopt/__init__.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/hyperopt/base.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/hyperopt/hyperband.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/parser.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/utils.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato/xyz.py +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato.egg-info/SOURCES.txt +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato.egg-info/dependency_links.txt +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato.egg-info/requires.txt +0 -0
- {ato-2.0.0 → ato-2.1.0}/ato.egg-info/top_level.txt +0 -0
- {ato-2.0.0 → ato-2.1.0}/setup.cfg +0 -0
{ato-2.0.0 → ato-2.1.0}/PKG-INFO
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ato
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Configuration, experimentation, and hyperparameter optimization for Python. No runtime magic. No launcher. Just Python modules you compose.
|
|
5
5
|
Author: ato contributors
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/yourusername/ato
|
|
8
8
|
Project-URL: Repository, https://github.com/yourusername/ato
|
|
9
9
|
Project-URL: Documentation, https://github.com/yourusername/ato#readme
|
|
10
10
|
Project-URL: Issues, https://github.com/yourusername/ato/issues
|
|
11
|
-
Keywords:
|
|
11
|
+
Keywords: config management,experiment tracking,hyperparameter optimization,lightweight,composable,namespace isolation,machine learning
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -32,38 +32,47 @@ Provides-Extra: distributed
|
|
|
32
32
|
Requires-Dist: torch>=1.8.0; extra == "distributed"
|
|
33
33
|
Dynamic: license-file
|
|
34
34
|
|
|
35
|
-
# Ato
|
|
35
|
+
# Ato: A Tiny Orchestrator
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
**Configuration, experimentation, and hyperparameter optimization for Python.**
|
|
38
|
+
|
|
39
|
+
No runtime magic. No launcher. No platform.
|
|
40
|
+
Just Python modules you compose.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install ato
|
|
44
|
+
```
|
|
40
45
|
|
|
41
46
|
---
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
It provides flexible configuration management, experiment tracking, and hyperparameter optimization —
|
|
45
|
-
all without the complexity or overhead of heavy frameworks.
|
|
48
|
+
## Design Philosophy
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
Ato was built on three constraints:
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
1. **Visibility** — When configs merge from multiple sources, you should see **why** a value was set.
|
|
53
|
+
2. **Composability** — Each module (ADict, Scope, SQLTracker, HyperOpt) works independently. Use one, use all, or mix with other tools.
|
|
54
|
+
3. **Structural neutrality** — Ato is a layer, not a platform. It has no opinion on your stack.
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- **Built-in Experiment Tracking**: SQLite-based tracking with no external services required
|
|
54
|
-
- **Structural Hashing**: Track experiment structure changes automatically
|
|
56
|
+
This isn't minimalism for its own sake.
|
|
57
|
+
It's **structural restraint** — interfering only where necessary, staying out of the way everywhere else.
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
**What Ato provides:**
|
|
60
|
+
- **Config composition** with explicit priority and merge order debugging
|
|
61
|
+
- **Namespace isolation** for multi-team projects (MultiScope)
|
|
62
|
+
- **Experiment tracking** in local SQLite with zero setup
|
|
63
|
+
- **Hyperparameter search** via Hyperband (or compose with Optuna/Ray Tune)
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
65
|
+
**What Ato doesn't provide:**
|
|
66
|
+
- Web dashboards (use MLflow/W&B)
|
|
67
|
+
- Model registry (use MLflow)
|
|
68
|
+
- Dataset versioning (use DVC)
|
|
69
|
+
- Plugin marketplace
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
Ato is designed to work **between** tools, not replace them.
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
67
76
|
|
|
68
77
|
### 30-Second Example
|
|
69
78
|
|
|
@@ -73,14 +82,14 @@ from ato.scope import Scope
|
|
|
73
82
|
scope = Scope()
|
|
74
83
|
|
|
75
84
|
@scope.observe(default=True)
|
|
76
|
-
def config(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
def config(config):
|
|
86
|
+
config.lr = 0.001
|
|
87
|
+
config.batch_size = 32
|
|
88
|
+
config.model = 'resnet50'
|
|
80
89
|
|
|
81
90
|
@scope
|
|
82
|
-
def train(
|
|
83
|
-
print(f"Training {
|
|
91
|
+
def train(config):
|
|
92
|
+
print(f"Training {config.model} with lr={config.lr}")
|
|
84
93
|
# Your training code here
|
|
85
94
|
|
|
86
95
|
if __name__ == '__main__':
|
|
@@ -88,72 +97,60 @@ if __name__ == '__main__':
|
|
|
88
97
|
# Override from CLI: python train.py lr=0.01 model=%resnet101%
|
|
89
98
|
```
|
|
90
99
|
|
|
100
|
+
**Key features:**
|
|
101
|
+
- `@scope.observe()` defines config sources
|
|
102
|
+
- `@scope` injects the merged config
|
|
103
|
+
- CLI overrides work automatically
|
|
104
|
+
- Priority-based merging (defaults → named configs → CLI → lazy evaluation)
|
|
105
|
+
|
|
91
106
|
---
|
|
92
107
|
|
|
93
108
|
## Table of Contents
|
|
94
109
|
|
|
95
110
|
- [ADict: Enhanced Dictionary](#adict-enhanced-dictionary)
|
|
96
111
|
- [Scope: Configuration Management](#scope-configuration-management)
|
|
97
|
-
- [MultiScope: Namespace Isolation](#
|
|
98
|
-
- [Config Documentation & Debugging](#
|
|
112
|
+
- [MultiScope: Namespace Isolation](#multiscope-namespace-isolation)
|
|
113
|
+
- [Config Documentation & Debugging](#configuration-documentation--debugging)
|
|
99
114
|
- [SQL Tracker: Experiment Tracking](#sql-tracker-experiment-tracking)
|
|
100
115
|
- [Hyperparameter Optimization](#hyperparameter-optimization)
|
|
101
116
|
- [Best Practices](#best-practices)
|
|
102
|
-
- [
|
|
117
|
+
- [Contributing](#contributing)
|
|
118
|
+
- [Composability](#composability)
|
|
103
119
|
|
|
104
120
|
---
|
|
105
121
|
|
|
106
122
|
## ADict: Enhanced Dictionary
|
|
107
123
|
|
|
108
|
-
`ADict` is an enhanced dictionary
|
|
124
|
+
`ADict` is an enhanced dictionary for managing experiment configurations.
|
|
109
125
|
|
|
110
126
|
### Core Features
|
|
111
127
|
|
|
112
|
-
These are the fundamental capabilities that make ADict powerful for experiment management:
|
|
113
|
-
|
|
114
128
|
| Feature | Description | Why It Matters |
|
|
115
129
|
|---------|-------------|----------------|
|
|
116
|
-
| **Structural Hashing** | Hash based on keys + types, not values | Track when experiment structure changes |
|
|
130
|
+
| **Structural Hashing** | Hash based on keys + types, not values | Track when experiment **structure** changes (not just hyperparameters) |
|
|
117
131
|
| **Nested Access** | Dot notation for nested configs | `config.model.lr` instead of `config['model']['lr']` |
|
|
118
132
|
| **Format Agnostic** | Load/save JSON, YAML, TOML, XYZ | Work with any config format |
|
|
119
|
-
| **Safe Updates** | `update_if_absent()` method |
|
|
133
|
+
| **Safe Updates** | `update_if_absent()` method | Merge configs without accidental overwrites |
|
|
134
|
+
| **Auto-nested** | `ADict.auto()` for lazy creation | `config.a.b.c = 1` just works - no KeyError |
|
|
120
135
|
|
|
121
|
-
###
|
|
136
|
+
### Examples
|
|
122
137
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
| Feature | Description | Benefit |
|
|
126
|
-
|---------|-------------|---------|
|
|
127
|
-
| **Auto-nested (`ADict.auto()`)** | Infinite depth lazy creation | `config.a.b.c = 1` just works - no KeyError |
|
|
128
|
-
| **Attribute-style Assignment** | `config.lr = 0.1` | Cleaner, more readable code |
|
|
129
|
-
| **Conditional Updates** | Only update missing keys | Merge configs safely |
|
|
130
|
-
|
|
131
|
-
### Quick Examples
|
|
138
|
+
#### Structural Hashing
|
|
132
139
|
|
|
133
140
|
```python
|
|
134
141
|
from ato.adict import ADict
|
|
135
142
|
|
|
136
|
-
#
|
|
143
|
+
# Same structure, different values
|
|
137
144
|
config1 = ADict(lr=0.1, epochs=100, model='resnet50')
|
|
138
145
|
config2 = ADict(lr=0.01, epochs=200, model='resnet101')
|
|
139
146
|
print(config1.get_structural_hash() == config2.get_structural_hash()) # True
|
|
140
147
|
|
|
141
|
-
|
|
148
|
+
# Different structure (epochs is str!)
|
|
149
|
+
config3 = ADict(lr=0.1, epochs='100', model='resnet50')
|
|
142
150
|
print(config1.get_structural_hash() == config3.get_structural_hash()) # False
|
|
143
|
-
|
|
144
|
-
# Load/save any format
|
|
145
|
-
config = ADict.from_file('config.json')
|
|
146
|
-
config.dump('config.yaml')
|
|
147
|
-
|
|
148
|
-
# Safe updates
|
|
149
|
-
config.update_if_absent(lr=0.01, scheduler='cosine') # Only adds scheduler
|
|
150
151
|
```
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#### Auto-nested: Zero Boilerplate Config Building
|
|
155
|
-
|
|
156
|
-
The most loved feature - no more manual nesting:
|
|
153
|
+
#### Auto-nested Configs
|
|
157
154
|
|
|
158
155
|
```python
|
|
159
156
|
# ❌ Traditional way
|
|
@@ -168,48 +165,24 @@ config.model.backbone.layers = [64, 128, 256] # Just works!
|
|
|
168
165
|
config.data.augmentation.brightness = 0.2
|
|
169
166
|
```
|
|
170
167
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```python
|
|
174
|
-
from ato.scope import Scope
|
|
175
|
-
|
|
176
|
-
scope = Scope()
|
|
177
|
-
|
|
178
|
-
@scope.observe(default=True)
|
|
179
|
-
def config(cfg):
|
|
180
|
-
# No pre-definition needed!
|
|
181
|
-
cfg.training.optimizer.name = 'AdamW'
|
|
182
|
-
cfg.training.optimizer.lr = 0.001
|
|
183
|
-
cfg.model.encoder.num_layers = 12
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
**Works with CLI**:
|
|
187
|
-
|
|
188
|
-
```bash
|
|
189
|
-
python train.py model.backbone.resnet.depth=50 data.batch_size=32
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
#### More Convenience Utilities
|
|
168
|
+
#### Format Agnostic
|
|
193
169
|
|
|
194
170
|
```python
|
|
195
|
-
#
|
|
196
|
-
config
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# Nested access
|
|
200
|
-
print(config.model.backbone.type) # Clean and readable
|
|
171
|
+
# Load/save any format
|
|
172
|
+
config = ADict.from_file('config.json')
|
|
173
|
+
config.dump('config.yaml')
|
|
201
174
|
|
|
202
|
-
#
|
|
203
|
-
|
|
175
|
+
# Safe updates
|
|
176
|
+
config.update_if_absent(lr=0.01, scheduler='cosine') # Only adds scheduler
|
|
204
177
|
```
|
|
205
178
|
|
|
206
179
|
---
|
|
207
180
|
|
|
208
181
|
## Scope: Configuration Management
|
|
209
182
|
|
|
210
|
-
Scope
|
|
183
|
+
Scope manages configuration through **priority-based merging** and **CLI integration**.
|
|
211
184
|
|
|
212
|
-
### Key
|
|
185
|
+
### Key Concept: Priority Chain
|
|
213
186
|
|
|
214
187
|
```
|
|
215
188
|
Default Configs (priority=0)
|
|
@@ -249,17 +222,17 @@ if __name__ == '__main__':
|
|
|
249
222
|
|
|
250
223
|
```python
|
|
251
224
|
@scope.observe(default=True) # Always applied
|
|
252
|
-
def defaults(
|
|
253
|
-
|
|
254
|
-
|
|
225
|
+
def defaults(config):
|
|
226
|
+
config.lr = 0.001
|
|
227
|
+
config.epochs = 100
|
|
255
228
|
|
|
256
229
|
@scope.observe(priority=1) # Applied after defaults
|
|
257
|
-
def high_lr(
|
|
258
|
-
|
|
230
|
+
def high_lr(config):
|
|
231
|
+
config.lr = 0.01
|
|
259
232
|
|
|
260
233
|
@scope.observe(priority=2) # Applied last
|
|
261
|
-
def long_training(
|
|
262
|
-
|
|
234
|
+
def long_training(config):
|
|
235
|
+
config.epochs = 300
|
|
263
236
|
```
|
|
264
237
|
|
|
265
238
|
```bash
|
|
@@ -288,27 +261,25 @@ python train.py my_config lr=0.001 batch_size=128
|
|
|
288
261
|
|
|
289
262
|
**Note**: Wrap strings with `%` (e.g., `%resnet101%`) instead of quotes.
|
|
290
263
|
|
|
291
|
-
###
|
|
292
|
-
|
|
293
|
-
#### 1. Lazy Evaluation - Dynamic Configuration
|
|
264
|
+
### Lazy Evaluation
|
|
294
265
|
|
|
295
266
|
Sometimes you need configs that depend on other values set via CLI:
|
|
296
267
|
|
|
297
268
|
```python
|
|
298
269
|
@scope.observe()
|
|
299
|
-
def base_config(
|
|
300
|
-
|
|
301
|
-
|
|
270
|
+
def base_config(config):
|
|
271
|
+
config.model = 'resnet50'
|
|
272
|
+
config.dataset = 'imagenet'
|
|
302
273
|
|
|
303
274
|
@scope.observe(lazy=True) # Evaluated AFTER CLI args
|
|
304
|
-
def computed_config(
|
|
275
|
+
def computed_config(config):
|
|
305
276
|
# Adjust based on dataset
|
|
306
|
-
if
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
elif
|
|
310
|
-
|
|
311
|
-
|
|
277
|
+
if config.dataset == 'imagenet':
|
|
278
|
+
config.num_classes = 1000
|
|
279
|
+
config.image_size = 224
|
|
280
|
+
elif config.dataset == 'cifar10':
|
|
281
|
+
config.num_classes = 10
|
|
282
|
+
config.image_size = 32
|
|
312
283
|
```
|
|
313
284
|
|
|
314
285
|
```bash
|
|
@@ -320,29 +291,20 @@ python train.py dataset=%cifar10% computed_config
|
|
|
320
291
|
|
|
321
292
|
```python
|
|
322
293
|
@scope.observe()
|
|
323
|
-
def my_config(
|
|
324
|
-
|
|
325
|
-
|
|
294
|
+
def my_config(config):
|
|
295
|
+
config.model = 'resnet50'
|
|
296
|
+
config.num_layers = 50
|
|
326
297
|
|
|
327
298
|
with Scope.lazy(): # Evaluated after CLI
|
|
328
|
-
if
|
|
329
|
-
|
|
299
|
+
if config.model == 'resnet101':
|
|
300
|
+
config.num_layers = 101
|
|
330
301
|
```
|
|
331
302
|
|
|
332
|
-
|
|
303
|
+
### MultiScope: Namespace Isolation
|
|
333
304
|
|
|
334
|
-
|
|
305
|
+
Manage completely separate configuration namespaces with independent priority systems.
|
|
335
306
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
| Challenge | Hydra's Approach | Ato's MultiScope |
|
|
339
|
-
|-----------|------------------|---------------------|
|
|
340
|
-
| Separate model/data configs | Config groups in one namespace | **Independent scopes with own priorities** |
|
|
341
|
-
| Avoid key collisions | Manual prefixing (`model.lr`, `train.lr`) | **Automatic namespace isolation** |
|
|
342
|
-
| Different teams/modules | Single config file | **Each scope can be owned separately** |
|
|
343
|
-
| Priority conflicts | Global priority system | **Per-scope priority system** |
|
|
344
|
-
|
|
345
|
-
##### Basic Usage
|
|
307
|
+
**Use case**: Different teams own different scopes without key collisions.
|
|
346
308
|
|
|
347
309
|
```python
|
|
348
310
|
from ato.scope import Scope, MultiScope
|
|
@@ -354,84 +316,103 @@ scope = MultiScope(model_scope, data_scope)
|
|
|
354
316
|
@model_scope.observe(default=True)
|
|
355
317
|
def model_config(model):
|
|
356
318
|
model.backbone = 'resnet50'
|
|
357
|
-
model.
|
|
319
|
+
model.lr = 0.1 # Model-specific learning rate
|
|
358
320
|
|
|
359
321
|
@data_scope.observe(default=True)
|
|
360
322
|
def data_config(data):
|
|
361
323
|
data.dataset = 'cifar10'
|
|
362
|
-
data.
|
|
324
|
+
data.lr = 0.001 # Data augmentation learning rate (no conflict!)
|
|
363
325
|
|
|
364
326
|
@scope
|
|
365
327
|
def train(model, data): # Named parameters match scope names
|
|
366
|
-
|
|
328
|
+
# Both have 'lr' but in separate namespaces!
|
|
329
|
+
print(f"Model LR: {model.lr}, Data LR: {data.lr}")
|
|
367
330
|
```
|
|
368
331
|
|
|
369
|
-
|
|
332
|
+
**Key advantage**: `model.lr` and `data.lr` are completely independent. No need for naming conventions like `model_lr` vs `data_lr`.
|
|
370
333
|
|
|
371
|
-
|
|
334
|
+
**CLI with MultiScope:**
|
|
372
335
|
|
|
373
|
-
```
|
|
374
|
-
#
|
|
375
|
-
|
|
336
|
+
```bash
|
|
337
|
+
# Override model scope only
|
|
338
|
+
python train.py model.backbone=%resnet101%
|
|
376
339
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
model.backbone = 'resnet50'
|
|
380
|
-
model.lr = 0.1 # Model-specific learning rate
|
|
340
|
+
# Override data scope only
|
|
341
|
+
python train.py data.dataset=%imagenet%
|
|
381
342
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
model.lr = 0.05 # Different lr for bigger model
|
|
343
|
+
# Override both
|
|
344
|
+
python train.py model.backbone=%resnet101% data.dataset=%imagenet%
|
|
345
|
+
```
|
|
386
346
|
|
|
387
|
-
|
|
388
|
-
data_scope = Scope(name='data')
|
|
347
|
+
### Configuration Documentation & Debugging
|
|
389
348
|
|
|
390
|
-
|
|
391
|
-
def cifar_default(data):
|
|
392
|
-
data.dataset = 'cifar10'
|
|
393
|
-
data.lr = 0.001 # Data augmentation learning rate (no conflict!)
|
|
349
|
+
**The `manual` command** visualizes the exact order of configuration application.
|
|
394
350
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
351
|
+
```python
|
|
352
|
+
@scope.observe(default=True)
|
|
353
|
+
def config(config):
|
|
354
|
+
config.lr = 0.001
|
|
355
|
+
config.batch_size = 32
|
|
356
|
+
config.model = 'resnet50'
|
|
399
357
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
358
|
+
@scope.manual
|
|
359
|
+
def config_docs(config):
|
|
360
|
+
config.lr = 'Learning rate for optimizer'
|
|
361
|
+
config.batch_size = 'Number of samples per batch'
|
|
362
|
+
config.model = 'Model architecture (resnet50, resnet101, etc.)'
|
|
363
|
+
```
|
|
403
364
|
|
|
404
|
-
|
|
365
|
+
```bash
|
|
366
|
+
python train.py manual
|
|
367
|
+
```
|
|
405
368
|
|
|
406
|
-
|
|
407
|
-
def train(model, data):
|
|
408
|
-
# Both have 'lr' but in separate namespaces!
|
|
409
|
-
print(f"Model LR: {model.lr}, Data LR: {data.lr}")
|
|
369
|
+
**Output:**
|
|
410
370
|
```
|
|
371
|
+
--------------------------------------------------
|
|
372
|
+
[Scope "config"]
|
|
373
|
+
(The Applying Order of Views)
|
|
374
|
+
config → (CLI Inputs)
|
|
411
375
|
|
|
412
|
-
|
|
376
|
+
(User Manuals)
|
|
377
|
+
lr: Learning rate for optimizer
|
|
378
|
+
batch_size: Number of samples per batch
|
|
379
|
+
model: Model architecture (resnet50, resnet101, etc.)
|
|
380
|
+
--------------------------------------------------
|
|
381
|
+
```
|
|
413
382
|
|
|
414
|
-
|
|
383
|
+
**Why this matters:**
|
|
384
|
+
When debugging "why is this config value not what I expect?", you can see **exactly** which function set it and in what order.
|
|
415
385
|
|
|
416
|
-
|
|
386
|
+
**Complex example:**
|
|
417
387
|
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
|
|
388
|
+
```python
|
|
389
|
+
@scope.observe(default=True)
|
|
390
|
+
def defaults(config):
|
|
391
|
+
config.lr = 0.001
|
|
421
392
|
|
|
422
|
-
|
|
423
|
-
|
|
393
|
+
@scope.observe(priority=1)
|
|
394
|
+
def experiment_config(config):
|
|
395
|
+
config.lr = 0.01
|
|
424
396
|
|
|
425
|
-
|
|
426
|
-
|
|
397
|
+
@scope.observe(priority=2)
|
|
398
|
+
def another_config(config):
|
|
399
|
+
config.lr = 0.1
|
|
427
400
|
|
|
428
|
-
|
|
429
|
-
|
|
401
|
+
@scope.observe(lazy=True)
|
|
402
|
+
def adaptive_lr(config):
|
|
403
|
+
if config.batch_size > 64:
|
|
404
|
+
config.lr = config.lr * 2
|
|
430
405
|
```
|
|
431
406
|
|
|
432
|
-
|
|
407
|
+
When you run `python train.py manual`, you see:
|
|
408
|
+
```
|
|
409
|
+
(The Applying Order of Views)
|
|
410
|
+
defaults → experiment_config → another_config → (CLI Inputs) → adaptive_lr
|
|
411
|
+
```
|
|
433
412
|
|
|
434
|
-
|
|
413
|
+
Now it's **crystal clear** why `lr=0.1` (from `another_config`) and not `0.01`!
|
|
414
|
+
|
|
415
|
+
### Config Import/Export
|
|
435
416
|
|
|
436
417
|
```python
|
|
437
418
|
@scope.observe()
|
|
@@ -442,36 +423,26 @@ def load_external(config):
|
|
|
442
423
|
|
|
443
424
|
# Export to any format
|
|
444
425
|
config.dump('output/final_config.toml')
|
|
445
|
-
|
|
446
|
-
# Import OpenMMLab configs - handles _base_ inheritance automatically
|
|
447
|
-
config.load_mm_config('mmdet_configs/faster_rcnn.py')
|
|
448
426
|
```
|
|
449
427
|
|
|
450
|
-
**OpenMMLab compatibility
|
|
451
|
-
- Automatically resolves `_base_` inheritance chains
|
|
452
|
-
- Supports `_delete_` keys for config overriding
|
|
453
|
-
- Makes migration from MMDetection/MMSegmentation/etc. seamless
|
|
428
|
+
**OpenMMLab compatibility:**
|
|
454
429
|
|
|
455
|
-
|
|
430
|
+
```python
|
|
431
|
+
# Import OpenMMLab configs - handles _base_ inheritance automatically
|
|
432
|
+
config.load_mm_config('mmdet_configs/faster_rcnn.py')
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Hierarchical composition:**
|
|
456
436
|
|
|
457
437
|
```python
|
|
458
438
|
from ato.adict import ADict
|
|
459
439
|
|
|
460
|
-
#
|
|
461
|
-
# configs/
|
|
462
|
-
# ├── config.yaml # base config
|
|
463
|
-
# ├── model/
|
|
464
|
-
# │ ├── resnet50.yaml
|
|
465
|
-
# │ └── resnet101.yaml
|
|
466
|
-
# └── data/
|
|
467
|
-
# ├── cifar10.yaml
|
|
468
|
-
# └── imagenet.yaml
|
|
469
|
-
|
|
440
|
+
# Load configs from directory structure
|
|
470
441
|
config = ADict.compose_hierarchy(
|
|
471
442
|
root='configs',
|
|
472
443
|
config_filename='config',
|
|
473
444
|
select={
|
|
474
|
-
'model': 'resnet50',
|
|
445
|
+
'model': 'resnet50',
|
|
475
446
|
'data': 'imagenet'
|
|
476
447
|
},
|
|
477
448
|
overrides={
|
|
@@ -483,16 +454,7 @@ config = ADict.compose_hierarchy(
|
|
|
483
454
|
)
|
|
484
455
|
```
|
|
485
456
|
|
|
486
|
-
|
|
487
|
-
- Config groups (model/, data/, optimizer/, etc.)
|
|
488
|
-
- Automatic file discovery (tries .yaml, .json, .toml, .xyz)
|
|
489
|
-
- Dotted overrides (`model.lr=0.01`)
|
|
490
|
-
- Required key validation
|
|
491
|
-
- Flexible error handling
|
|
492
|
-
|
|
493
|
-
#### 4. Argparse Integration
|
|
494
|
-
|
|
495
|
-
Mix Ato with existing argparse code:
|
|
457
|
+
### Argparse Integration
|
|
496
458
|
|
|
497
459
|
```python
|
|
498
460
|
from ato.scope import Scope
|
|
@@ -504,170 +466,24 @@ parser.add_argument('--gpu', type=int, default=0)
|
|
|
504
466
|
parser.add_argument('--seed', type=int, default=42)
|
|
505
467
|
|
|
506
468
|
@scope.observe(default=True)
|
|
507
|
-
def config(
|
|
508
|
-
|
|
509
|
-
|
|
469
|
+
def config(config):
|
|
470
|
+
config.lr = 0.001
|
|
471
|
+
config.batch_size = 32
|
|
510
472
|
|
|
511
473
|
@scope
|
|
512
|
-
def train(
|
|
513
|
-
print(f"GPU: {
|
|
474
|
+
def train(config):
|
|
475
|
+
print(f"GPU: {config.gpu}, LR: {config.lr}")
|
|
514
476
|
|
|
515
477
|
if __name__ == '__main__':
|
|
516
478
|
parser.parse_args() # Merges argparse with scope
|
|
517
479
|
train()
|
|
518
480
|
```
|
|
519
481
|
|
|
520
|
-
#### 5. Configuration Documentation & Inspection
|
|
521
|
-
|
|
522
|
-
**One of Ato's most powerful features**: Auto-generate documentation AND visualize the exact order of configuration application.
|
|
523
|
-
|
|
524
|
-
##### Basic Documentation
|
|
525
|
-
|
|
526
|
-
```python
|
|
527
|
-
@scope.manual
|
|
528
|
-
def config_docs(cfg):
|
|
529
|
-
cfg.lr = 'Learning rate for optimizer'
|
|
530
|
-
cfg.batch_size = 'Number of samples per batch'
|
|
531
|
-
cfg.model = 'Model architecture (resnet50, resnet101, etc.)'
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
```bash
|
|
535
|
-
python train.py manual
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
**Output:**
|
|
539
|
-
```
|
|
540
|
-
--------------------------------------------------
|
|
541
|
-
[Scope "config"]
|
|
542
|
-
(The Applying Order of Views)
|
|
543
|
-
defaults → (CLI Inputs) → lazy_config → main
|
|
544
|
-
|
|
545
|
-
(User Manuals)
|
|
546
|
-
config.lr: Learning rate for optimizer
|
|
547
|
-
config.batch_size: Number of samples per batch
|
|
548
|
-
config.model: Model architecture (resnet50, resnet101, etc.)
|
|
549
|
-
--------------------------------------------------
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
##### Why This Matters
|
|
553
|
-
|
|
554
|
-
The **applying order visualization** shows you **exactly** how your configs are merged:
|
|
555
|
-
- Which config functions are applied (in order)
|
|
556
|
-
- When CLI inputs override values
|
|
557
|
-
- Where lazy configs are evaluated
|
|
558
|
-
- The final function that uses the config
|
|
559
|
-
|
|
560
|
-
**This prevents configuration bugs** by making the merge order explicit and debuggable.
|
|
561
|
-
|
|
562
|
-
##### MultiScope Documentation
|
|
563
|
-
|
|
564
|
-
For complex projects with multiple scopes, `manual` shows each scope separately:
|
|
565
|
-
|
|
566
|
-
```python
|
|
567
|
-
from ato.scope import Scope, MultiScope
|
|
568
|
-
|
|
569
|
-
model_scope = Scope(name='model')
|
|
570
|
-
train_scope = Scope(name='train')
|
|
571
|
-
scope = MultiScope(model_scope, train_scope)
|
|
572
|
-
|
|
573
|
-
@model_scope.observe(default=True)
|
|
574
|
-
def model_defaults(model):
|
|
575
|
-
model.backbone = 'resnet50'
|
|
576
|
-
model.num_layers = 50
|
|
577
|
-
|
|
578
|
-
@model_scope.observe(priority=1)
|
|
579
|
-
def model_advanced(model):
|
|
580
|
-
model.pretrained = True
|
|
581
|
-
|
|
582
|
-
@model_scope.observe(lazy=True)
|
|
583
|
-
def model_lazy(model):
|
|
584
|
-
if model.backbone == 'resnet101':
|
|
585
|
-
model.num_layers = 101
|
|
586
|
-
|
|
587
|
-
@train_scope.observe(default=True)
|
|
588
|
-
def train_defaults(train):
|
|
589
|
-
train.lr = 0.001
|
|
590
|
-
train.epochs = 100
|
|
591
|
-
|
|
592
|
-
@model_scope.manual
|
|
593
|
-
def model_docs(model):
|
|
594
|
-
model.backbone = 'Model backbone architecture'
|
|
595
|
-
model.num_layers = 'Number of layers in the model'
|
|
596
|
-
|
|
597
|
-
@train_scope.manual
|
|
598
|
-
def train_docs(train):
|
|
599
|
-
train.lr = 'Learning rate for optimizer'
|
|
600
|
-
train.epochs = 'Total training epochs'
|
|
601
|
-
|
|
602
|
-
@scope
|
|
603
|
-
def main(model, train):
|
|
604
|
-
print(f"Training {model.backbone} with lr={train.lr}")
|
|
605
|
-
|
|
606
|
-
if __name__ == '__main__':
|
|
607
|
-
main()
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
```bash
|
|
611
|
-
python train.py manual
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
**Output:**
|
|
615
|
-
```
|
|
616
|
-
--------------------------------------------------
|
|
617
|
-
[Scope "model"]
|
|
618
|
-
(The Applying Order of Views)
|
|
619
|
-
model_defaults → model_advanced → (CLI Inputs) → model_lazy → main
|
|
620
|
-
|
|
621
|
-
(User Manuals)
|
|
622
|
-
model.backbone: Model backbone architecture
|
|
623
|
-
model.num_layers: Number of layers in the model
|
|
624
|
-
--------------------------------------------------
|
|
625
|
-
[Scope "train"]
|
|
626
|
-
(The Applying Order of Views)
|
|
627
|
-
train_defaults → (CLI Inputs) → main
|
|
628
|
-
|
|
629
|
-
(User Manuals)
|
|
630
|
-
train.lr: Learning rate for optimizer
|
|
631
|
-
train.epochs: Total training epochs
|
|
632
|
-
--------------------------------------------------
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
##### Real-world Example
|
|
636
|
-
|
|
637
|
-
This is especially valuable when debugging why a config value isn't what you expect:
|
|
638
|
-
|
|
639
|
-
```python
|
|
640
|
-
@scope.observe(default=True)
|
|
641
|
-
def defaults(cfg):
|
|
642
|
-
cfg.lr = 0.001
|
|
643
|
-
|
|
644
|
-
@scope.observe(priority=1)
|
|
645
|
-
def experiment_config(cfg):
|
|
646
|
-
cfg.lr = 0.01
|
|
647
|
-
|
|
648
|
-
@scope.observe(priority=2)
|
|
649
|
-
def another_config(cfg):
|
|
650
|
-
cfg.lr = 0.1
|
|
651
|
-
|
|
652
|
-
@scope.observe(lazy=True)
|
|
653
|
-
def adaptive_lr(cfg):
|
|
654
|
-
if cfg.batch_size > 64:
|
|
655
|
-
cfg.lr = cfg.lr * 2
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
When you run `python train.py manual`, you see:
|
|
659
|
-
```
|
|
660
|
-
(The Applying Order of Views)
|
|
661
|
-
defaults → experiment_config → another_config → (CLI Inputs) → adaptive_lr → main
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
Now it's **crystal clear** why `lr=0.1` (from `another_config`) and not `0.01`!
|
|
665
|
-
|
|
666
482
|
---
|
|
667
483
|
|
|
668
484
|
## SQL Tracker: Experiment Tracking
|
|
669
485
|
|
|
670
|
-
Lightweight experiment tracking using SQLite
|
|
486
|
+
Lightweight experiment tracking using SQLite.
|
|
671
487
|
|
|
672
488
|
### Why SQL Tracker?
|
|
673
489
|
|
|
@@ -675,6 +491,7 @@ Lightweight experiment tracking using SQLite - no external services, no setup co
|
|
|
675
491
|
- **Full History**: Track all runs, metrics, and artifacts
|
|
676
492
|
- **Smart Search**: Find similar experiments by config structure
|
|
677
493
|
- **Code Versioning**: Track code changes via fingerprints
|
|
494
|
+
- **Offline-first**: No network required, sync to cloud tracking later if needed
|
|
678
495
|
|
|
679
496
|
### Database Schema
|
|
680
497
|
|
|
@@ -690,7 +507,7 @@ Project (my_ml_project)
|
|
|
690
507
|
└── ...
|
|
691
508
|
```
|
|
692
509
|
|
|
693
|
-
###
|
|
510
|
+
### Usage
|
|
694
511
|
|
|
695
512
|
#### Logging Experiments
|
|
696
513
|
|
|
@@ -764,22 +581,7 @@ stats = finder.get_trace_statistics('image_classification', trace_id='model_forw
|
|
|
764
581
|
print(f"Model forward pass has {stats['static_trace_versions']} versions")
|
|
765
582
|
```
|
|
766
583
|
|
|
767
|
-
###
|
|
768
|
-
|
|
769
|
-
```python
|
|
770
|
-
# Compare hyperparameter impact
|
|
771
|
-
finder = SQLFinder(config)
|
|
772
|
-
|
|
773
|
-
runs = finder.get_runs_in_project('my_project')
|
|
774
|
-
for run in runs:
|
|
775
|
-
# Get final accuracy
|
|
776
|
-
final_metrics = [m for m in run.metrics if m.key == 'val_accuracy']
|
|
777
|
-
best_acc = max(m.value for m in final_metrics) if final_metrics else 0
|
|
778
|
-
|
|
779
|
-
print(f"LR: {run.config.lr}, Batch: {run.config.batch_size} → Acc: {best_acc:.2%}")
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
### Features Summary
|
|
584
|
+
### Features
|
|
783
585
|
|
|
784
586
|
| Feature | Description |
|
|
785
587
|
|---------|-------------|
|
|
@@ -795,38 +597,6 @@ for run in runs:
|
|
|
795
597
|
|
|
796
598
|
Built-in **Hyperband** algorithm for efficient hyperparameter search with early stopping.
|
|
797
599
|
|
|
798
|
-
### Extensible Design
|
|
799
|
-
|
|
800
|
-
Ato's hyperopt module is built for extensibility and reusability:
|
|
801
|
-
|
|
802
|
-
| Component | Purpose | Benefit |
|
|
803
|
-
|-----------|---------|---------|
|
|
804
|
-
| `GridSpaceMixIn` | Parameter sampling logic | Reusable across different algorithms |
|
|
805
|
-
| `HyperOpt` | Base optimization class | Easy to implement custom strategies |
|
|
806
|
-
| `DistributedMixIn` | Distributed training support | Optional, composable |
|
|
807
|
-
|
|
808
|
-
**This design makes it trivial to implement custom search algorithms**:
|
|
809
|
-
|
|
810
|
-
```python
|
|
811
|
-
from ato.hyperopt.base import GridSpaceMixIn, HyperOpt
|
|
812
|
-
|
|
813
|
-
class RandomSearch(GridSpaceMixIn, HyperOpt):
|
|
814
|
-
def main(self, func):
|
|
815
|
-
# Reuse GridSpaceMixIn.prepare_distributions()
|
|
816
|
-
configs = self.prepare_distributions(self.config, self.search_spaces)
|
|
817
|
-
|
|
818
|
-
# Implement random sampling
|
|
819
|
-
import random
|
|
820
|
-
random.shuffle(configs)
|
|
821
|
-
|
|
822
|
-
results = []
|
|
823
|
-
for config in configs[:10]: # Sample 10 random configs
|
|
824
|
-
metric = func(config)
|
|
825
|
-
results.append((config, metric))
|
|
826
|
-
|
|
827
|
-
return max(results, key=lambda x: x[1])
|
|
828
|
-
```
|
|
829
|
-
|
|
830
600
|
### How Hyperband Works
|
|
831
601
|
|
|
832
602
|
Hyperband uses successive halving:
|
|
@@ -895,8 +665,6 @@ if __name__ == '__main__':
|
|
|
895
665
|
|
|
896
666
|
### Automatic Step Calculation
|
|
897
667
|
|
|
898
|
-
Let Hyperband compute optimal training steps:
|
|
899
|
-
|
|
900
668
|
```python
|
|
901
669
|
hyperband = HyperBand(scope, search_spaces, halving_rate=0.3, num_min_samples=4)
|
|
902
670
|
|
|
@@ -926,9 +694,7 @@ Space types:
|
|
|
926
694
|
- `LOG`: Logarithmic spacing (good for learning rates)
|
|
927
695
|
- `LINEAR`: Linear spacing (default)
|
|
928
696
|
|
|
929
|
-
### Distributed
|
|
930
|
-
|
|
931
|
-
Ato supports distributed hyperparameter optimization out of the box:
|
|
697
|
+
### Distributed Search
|
|
932
698
|
|
|
933
699
|
```python
|
|
934
700
|
from ato.hyperopt.hyperband import DistributedHyperBand
|
|
@@ -965,11 +731,37 @@ if __name__ == '__main__':
|
|
|
965
731
|
print(f"Best config: {result.config}")
|
|
966
732
|
```
|
|
967
733
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
734
|
+
### Extensible Design
|
|
735
|
+
|
|
736
|
+
Ato's hyperopt module is built for extensibility:
|
|
737
|
+
|
|
738
|
+
| Component | Purpose |
|
|
739
|
+
|-----------|---------|
|
|
740
|
+
| `GridSpaceMixIn` | Parameter sampling logic (reusable) |
|
|
741
|
+
| `HyperOpt` | Base optimization class |
|
|
742
|
+
| `DistributedMixIn` | Distributed training support (optional) |
|
|
743
|
+
|
|
744
|
+
**Example: Implement custom search algorithm**
|
|
745
|
+
|
|
746
|
+
```python
|
|
747
|
+
from ato.hyperopt.base import GridSpaceMixIn, HyperOpt
|
|
748
|
+
|
|
749
|
+
class RandomSearch(GridSpaceMixIn, HyperOpt):
|
|
750
|
+
def main(self, func):
|
|
751
|
+
# Reuse GridSpaceMixIn.prepare_distributions()
|
|
752
|
+
configs = self.prepare_distributions(self.config, self.search_spaces)
|
|
753
|
+
|
|
754
|
+
# Implement random sampling
|
|
755
|
+
import random
|
|
756
|
+
random.shuffle(configs)
|
|
757
|
+
|
|
758
|
+
results = []
|
|
759
|
+
for config in configs[:10]: # Sample 10 random configs
|
|
760
|
+
metric = func(config)
|
|
761
|
+
results.append((config, metric))
|
|
762
|
+
|
|
763
|
+
return max(results, key=lambda x: x[1])
|
|
764
|
+
```
|
|
973
765
|
|
|
974
766
|
---
|
|
975
767
|
|
|
@@ -997,33 +789,34 @@ my_project/
|
|
|
997
789
|
```python
|
|
998
790
|
# configs/default.py
|
|
999
791
|
from ato.scope import Scope
|
|
792
|
+
from ato.adict import ADict
|
|
1000
793
|
|
|
1001
794
|
scope = Scope()
|
|
1002
795
|
|
|
1003
796
|
@scope.observe(default=True)
|
|
1004
|
-
def defaults(
|
|
797
|
+
def defaults(config):
|
|
1005
798
|
# Data
|
|
1006
|
-
|
|
799
|
+
config.data = ADict(
|
|
1007
800
|
dataset='cifar10',
|
|
1008
801
|
batch_size=32,
|
|
1009
802
|
num_workers=4
|
|
1010
803
|
)
|
|
1011
804
|
|
|
1012
805
|
# Model
|
|
1013
|
-
|
|
806
|
+
config.model = ADict(
|
|
1014
807
|
backbone='resnet50',
|
|
1015
808
|
pretrained=True
|
|
1016
809
|
)
|
|
1017
810
|
|
|
1018
811
|
# Training
|
|
1019
|
-
|
|
812
|
+
config.train = ADict(
|
|
1020
813
|
lr=0.001,
|
|
1021
814
|
epochs=100,
|
|
1022
815
|
optimizer='adam'
|
|
1023
816
|
)
|
|
1024
817
|
|
|
1025
818
|
# Experiment tracking
|
|
1026
|
-
|
|
819
|
+
config.experiment = ADict(
|
|
1027
820
|
project_name='my_project',
|
|
1028
821
|
sql=ADict(db_path='sqlite:///experiments.db')
|
|
1029
822
|
)
|
|
@@ -1037,14 +830,14 @@ from ato.db_routers.sql.manager import SQLLogger
|
|
|
1037
830
|
from configs.default import scope
|
|
1038
831
|
|
|
1039
832
|
@scope
|
|
1040
|
-
def train(
|
|
833
|
+
def train(config):
|
|
1041
834
|
# Setup experiment tracking
|
|
1042
|
-
logger = SQLLogger(
|
|
1043
|
-
run_id = logger.run(tags=[
|
|
835
|
+
logger = SQLLogger(config)
|
|
836
|
+
run_id = logger.run(tags=[config.model.backbone, config.data.dataset])
|
|
1044
837
|
|
|
1045
838
|
try:
|
|
1046
839
|
# Training loop
|
|
1047
|
-
for epoch in range(
|
|
840
|
+
for epoch in range(config.train.epochs):
|
|
1048
841
|
loss = train_epoch()
|
|
1049
842
|
acc = validate()
|
|
1050
843
|
|
|
@@ -1082,12 +875,6 @@ See `pyproject.toml` for full dependencies.
|
|
|
1082
875
|
|
|
1083
876
|
---
|
|
1084
877
|
|
|
1085
|
-
## License
|
|
1086
|
-
|
|
1087
|
-
MIT License
|
|
1088
|
-
|
|
1089
|
-
---
|
|
1090
|
-
|
|
1091
878
|
## Contributing
|
|
1092
879
|
|
|
1093
880
|
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
@@ -1100,82 +887,92 @@ cd ato
|
|
|
1100
887
|
pip install -e .
|
|
1101
888
|
```
|
|
1102
889
|
|
|
890
|
+
### Quality Assurance
|
|
891
|
+
|
|
892
|
+
Ato's design philosophy — **structural neutrality** and **debuggable composition** — extends to our testing practices.
|
|
893
|
+
|
|
894
|
+
**Release Policy:**
|
|
895
|
+
- **All 100+ unit tests must pass before any release**
|
|
896
|
+
- No exceptions, no workarounds
|
|
897
|
+
- Tests cover every module: ADict, Scope, MultiScope, SQLTracker, HyperBand
|
|
898
|
+
|
|
899
|
+
**Why this matters:**
|
|
900
|
+
When you build on Ato, you're trusting it to stay out of your way. That means zero regressions, predictable behavior, and reliable APIs. Comprehensive test coverage ensures that each component works independently and composes correctly.
|
|
901
|
+
|
|
902
|
+
Run tests locally:
|
|
903
|
+
```bash
|
|
904
|
+
python -m pytest unit_tests/
|
|
905
|
+
```
|
|
906
|
+
|
|
1103
907
|
---
|
|
1104
908
|
|
|
1105
|
-
##
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
-
|
|
1159
|
-
-
|
|
1160
|
-
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
-
|
|
1166
|
-
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
**Ato is for you if:**
|
|
1177
|
-
- You want lightweight, offline-first experiment tracking
|
|
1178
|
-
- You need **true namespace isolation for team collaboration**
|
|
1179
|
-
- **You want to debug config merge order visually** (unique to Ato!)
|
|
1180
|
-
- You prefer simple Python over complex frameworks
|
|
1181
|
-
- You want reproducibility without overhead
|
|
909
|
+
## Composability
|
|
910
|
+
|
|
911
|
+
Ato is designed to **compose** with existing tools, not replace them.
|
|
912
|
+
|
|
913
|
+
### Works Where Other Systems Require Ecosystems
|
|
914
|
+
|
|
915
|
+
**Config composition:**
|
|
916
|
+
- Import OpenMMLab configs: `config.load_mm_config('mmdet_configs/faster_rcnn.py')`
|
|
917
|
+
- Load Hydra-style hierarchies: `ADict.compose_hierarchy(root='configs', select={'model': 'resnet50'})`
|
|
918
|
+
- Mix with argparse: `Scope(use_external_parser=True)`
|
|
919
|
+
|
|
920
|
+
**Experiment tracking:**
|
|
921
|
+
- Track locally in SQLite (zero setup)
|
|
922
|
+
- Sync to MLflow/W&B when you need dashboards
|
|
923
|
+
- Or use both: local SQLite + cloud tracking
|
|
924
|
+
|
|
925
|
+
**Hyperparameter optimization:**
|
|
926
|
+
- Built-in Hyperband
|
|
927
|
+
- Or compose with Optuna/Ray Tune — Ato's configs work with any optimizer
|
|
928
|
+
|
|
929
|
+
### Three Capabilities Other Tools Don't Provide
|
|
930
|
+
|
|
931
|
+
1. **MultiScope** — True namespace isolation with independent priority systems
|
|
932
|
+
2. **`manual` command** — Visualize exact config merge order for debugging
|
|
933
|
+
3. **Structural hashing** — Track when experiment **architecture** changes, not just values
|
|
934
|
+
|
|
935
|
+
### When to Use Ato
|
|
936
|
+
|
|
937
|
+
**Use Ato when:**
|
|
938
|
+
- You want zero boilerplate config management
|
|
939
|
+
- You need to debug why a config value isn't what you expect
|
|
940
|
+
- You're working on multi-team projects with namespace conflicts
|
|
941
|
+
- You want local-first experiment tracking
|
|
942
|
+
- You're migrating between config/tracking systems
|
|
943
|
+
|
|
944
|
+
**Ato works alongside:**
|
|
945
|
+
- Hydra (config composition)
|
|
946
|
+
- MLflow/W&B (cloud tracking)
|
|
947
|
+
- Optuna/Ray Tune (advanced hyperparameter search)
|
|
948
|
+
- PyTorch/TensorFlow/JAX (any ML framework)
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
## Roadmap
|
|
953
|
+
|
|
954
|
+
Ato's design constraint is **structural neutrality** — adding capabilities without creating dependencies.
|
|
955
|
+
|
|
956
|
+
### Planned: Local Dashboard (Optional Module)
|
|
957
|
+
|
|
958
|
+
A lightweight HTML dashboard for teams that want visual exploration without committing to cloud platforms:
|
|
959
|
+
|
|
960
|
+
**What it adds:**
|
|
961
|
+
- Metric comparison & trends (read-only view of SQLite data)
|
|
962
|
+
- Run history & artifact browsing
|
|
963
|
+
- Config diff visualization
|
|
964
|
+
- Interactive hyperparameter analysis
|
|
965
|
+
|
|
966
|
+
**Design constraints:**
|
|
967
|
+
- No hard dependency — Ato core works 100% without the dashboard
|
|
968
|
+
- Separate process — doesn't block or modify runs
|
|
969
|
+
- Zero lock-in — delete it anytime, training code doesn't change
|
|
970
|
+
- Composable — use alongside MLflow/W&B
|
|
971
|
+
|
|
972
|
+
**Guiding principle:** Ato remains a set of **independent, composable tools** — not a platform you commit to.
|
|
973
|
+
|
|
974
|
+
---
|
|
975
|
+
|
|
976
|
+
## License
|
|
977
|
+
|
|
978
|
+
MIT License
|