hypergan 2.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hypergan/__init__.py +39 -0
- hypergan/__main__.py +5 -0
- hypergan/_build_provenance.json +1 -0
- hypergan/_file_lock.py +32 -0
- hypergan/api.py +2221 -0
- hypergan/artifacts.py +334 -0
- hypergan/autoencoder_components.py +60 -0
- hypergan/background_poll.py +74 -0
- hypergan/bounded_cli_output.py +562 -0
- hypergan/bounded_observer.py +278 -0
- hypergan/checkpoint_compatibility.py +203 -0
- hypergan/checkpoints.py +902 -0
- hypergan/cli.py +2165 -0
- hypergan/color_words.py +288 -0
- hypergan/colorization_components.py +360 -0
- hypergan/colorization_data.py +256 -0
- hypergan/colorization_metrics.py +168 -0
- hypergan/config.py +1041 -0
- hypergan/console_settings.py +87 -0
- hypergan/cpu_worker_service.py +575 -0
- hypergan/cpu_workers.py +125 -0
- hypergan/data.py +429 -0
- hypergan/datasets.py +6 -0
- hypergan/diff_augment.py +131 -0
- hypergan/distributed.py +110 -0
- hypergan/distributed_checkpoints.py +558 -0
- hypergan/distributed_commit.py +297 -0
- hypergan/distributed_training.py +733 -0
- hypergan/diversity_metrics.py +175 -0
- hypergan/evaluation_cli.py +22 -0
- hypergan/evaluation_snapshot.py +28 -0
- hypergan/event_views.py +500 -0
- hypergan/execution.py +360 -0
- hypergan/execution_preflight.py +264 -0
- hypergan/execution_profiles.py +142 -0
- hypergan/gpu_inventory.py +687 -0
- hypergan/hndl_augmentation.py +40 -0
- hypergan/hndl_networks.py +42 -0
- hypergan/image_cache.py +66 -0
- hypergan/image_components.py +326 -0
- hypergan/image_data.py +96 -0
- hypergan/image_grids.py +151 -0
- hypergan/image_metrics.py +121 -0
- hypergan/inspection.py +1956 -0
- hypergan/interval_evaluation.py +294 -0
- hypergan/item_data.py +165 -0
- hypergan/metric_evaluation.py +425 -0
- hypergan/metric_evaluation_worker.py +136 -0
- hypergan/metric_examples.py +81 -0
- hypergan/metric_plugins.py +339 -0
- hypergan/metrics.py +407 -0
- hypergan/metrics_reducer/__init__.py +12 -0
- hypergan/model_description.py +651 -0
- hypergan/model_file.py +1014 -0
- hypergan/models/gaussian-grid.toml +48 -0
- hypergan/network_config.py +131 -0
- hypergan/networks/autoencoder_offset.hndl +1 -0
- hypergan/networks/colorization_conv_head.hndl +6 -0
- hypergan/networks/colorization_dcgan256.hndl +14 -0
- hypergan/networks/colorization_dinov3_multidepth.hndl +20 -0
- hypergan/networks/colorization_dinov3_patch_tokens.hndl +19 -0
- hypergan/networks/colorization_encoder_features.hndl +19 -0
- hypergan/networks/colorization_feature_concat.hndl +2 -0
- hypergan/networks/colorization_feature_project.hndl +3 -0
- hypergan/networks/colorization_generator16.hndl +69 -0
- hypergan/networks/colorization_generator32.hndl +69 -0
- hypergan/networks/colorization_joint_score.hndl +2 -0
- hypergan/networks/colorization_linear_head.hndl +3 -0
- hypergan/networks/colorization_multidepth_projection.hndl +18 -0
- hypergan/networks/colorization_multiscale_score.hndl +2 -0
- hypergan/networks/colorization_pixel_features.hndl +8 -0
- hypergan/networks/colorization_pixel_head.hndl +17 -0
- hypergan/networks/colorization_query.hndl +2 -0
- hypergan/networks/colorization_random_project.hndl +2 -0
- hypergan/networks/colorization_scale_head16.hndl +8 -0
- hypergan/networks/colorization_scale_head32.hndl +10 -0
- hypergan/networks/colorization_scale_head4.hndl +4 -0
- hypergan/networks/colorization_scale_head8.hndl +6 -0
- hypergan/networks/image_attention.hndl +15 -0
- hypergan/networks/image_critic_score.hndl +6 -0
- hypergan/networks/image_encoder.hndl +17 -0
- hypergan/networks/image_feature_head.hndl +9 -0
- hypergan/networks/image_generator.hndl +13 -0
- hypergan/networks/image_pixel.hndl +7 -0
- hypergan/networks/image_pixel_block.hndl +16 -0
- hypergan/networks/image_resnet_stage1.hndl +7 -0
- hypergan/networks/image_resnet_stage2.hndl +2 -0
- hypergan/networks/image_resnet_stage3.hndl +2 -0
- hypergan/networks/reference.hndl +6 -0
- hypergan/numerical_policy.py +95 -0
- hypergan/objective_program.py +425 -0
- hypergan/observation_io.py +150 -0
- hypergan/particlegan_integration.py +134 -0
- hypergan/plugin_functions.py +94 -0
- hypergan/ports.py +9 -0
- hypergan/pretrained_providers.py +104 -0
- hypergan/preview_snapshot.py +229 -0
- hypergan/preview_worker.py +128 -0
- hypergan/previews.py +590 -0
- hypergan/provenance.py +187 -0
- hypergan/queue.py +819 -0
- hypergan/recipe_generator.py +691 -0
- hypergan/recipes.py +279 -0
- hypergan/registry.py +1337 -0
- hypergan/replicated_execution.py +487 -0
- hypergan/replicated_worker.py +184 -0
- hypergan/run_controller.py +990 -0
- hypergan/run_events.py +185 -0
- hypergan/run_requests.py +277 -0
- hypergan/run_signals.py +42 -0
- hypergan/run_state.py +80 -0
- hypergan/single_execution.py +225 -0
- hypergan/snapshot_renderer.py +71 -0
- hypergan/summary.py +336 -0
- hypergan/toml_writer.py +125 -0
- hypergan/training.py +543 -0
- hypergan/web_assets/THIRD_PARTY_LICENSES.txt +289 -0
- hypergan/web_assets/__init__.py +25 -0
- hypergan/web_assets/app.js +22 -0
- hypergan/web_assets/index.html +225 -0
- hypergan/web_assets/logo.png +0 -0
- hypergan/web_assets/runs.css +75 -0
- hypergan/web_assets/runs.js +203 -0
- hypergan/web_assets/style.css +871 -0
- hypergan/web_assets/view-worker.js +204 -0
- hypergan/web_autostart.py +1283 -0
- hypergan/web_dev.py +132 -0
- hypergan/web_files.py +83 -0
- hypergan/web_launch.py +272 -0
- hypergan/web_schema.py +165 -0
- hypergan/web_server.py +1323 -0
- hypergan/web_service.py +651 -0
- hypergan/web_session.py +191 -0
- hypergan/workspace.py +325 -0
- hypergan-2.0.0a1.dist-info/METADATA +328 -0
- hypergan-2.0.0a1.dist-info/RECORD +140 -0
- hypergan-2.0.0a1.dist-info/WHEEL +5 -0
- hypergan-2.0.0a1.dist-info/entry_points.txt +2 -0
- hypergan-2.0.0a1.dist-info/licenses/LICENSE +21 -0
- hypergan-2.0.0a1.dist-info/top_level.txt +1 -0
hypergan/api.py
ADDED
|
@@ -0,0 +1,2221 @@
|
|
|
1
|
+
"""HyperGAN's Python API: a thin, functional layer over model files.
|
|
2
|
+
|
|
3
|
+
The model file is the source of truth (see ``hypergan.model_file``). This module
|
|
4
|
+
loads one, lets a script change values, trains it and reads the run back::
|
|
5
|
+
|
|
6
|
+
import hypergan.api as hg
|
|
7
|
+
|
|
8
|
+
model = hg.load("model.toml")
|
|
9
|
+
model = hg.override(model, {"train.steps": 200, "losses.reconstruction.weight": 0.5})
|
|
10
|
+
run = hg.train(model, "runs/demo", previews=50)
|
|
11
|
+
hg.metrics(run)["loss/d_total"] # [(step, value), ...]
|
|
12
|
+
|
|
13
|
+
Everything here is a plain function over two data objects, ``Model`` (the
|
|
14
|
+
model file as a dictionary plus the directory its relative paths use) and
|
|
15
|
+
``Run`` (a run directory and its manifest). Nothing is copied into a run
|
|
16
|
+
directory: the run records the lowered recipe, and user code stays where the
|
|
17
|
+
model file's import paths point.
|
|
18
|
+
"""
|
|
19
|
+
from copy import deepcopy
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
import hashlib
|
|
22
|
+
import inspect as _py_inspect
|
|
23
|
+
import json
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
from .model_file import FORMAT, ConfigError, ModelFileError, is_model_file, lower as _lower, model_warnings, upstream, producer
|
|
28
|
+
from .toml_writer import dumps as _dumps
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import tomllib
|
|
32
|
+
except ModuleNotFoundError: # Python 3.10
|
|
33
|
+
import tomli as tomllib
|
|
34
|
+
|
|
35
|
+
from .workspace import find_workspace, load_workspace, interpolate_variables, resolve_device_aliases
|
|
36
|
+
from .model_file import resolve_inheritance, collect_source_files
|
|
37
|
+
|
|
38
|
+
PACKAGED = Path(__file__).with_name('models')
|
|
39
|
+
|
|
40
|
+
__all__ = ['Model', 'Run', 'Sample', 'Image', 'Audio', 'ModelFileError', 'ConfigError',
|
|
41
|
+
'model', 'load', 'load_bundle', 'packaged', 'packaged_list', 'from_dict', 'override', 'save', 'dumps', 'lower', 'resolve', 'validate',
|
|
42
|
+
'validate_config', 'init_project', 'new', 'list_templates',
|
|
43
|
+
'fingerprint', 'explain', 'import_path', 'plain', 'hndl', 'module', 'sampler', 'adversarial',
|
|
44
|
+
'reconstruction', 'objective', 'prior_loss', 'prepare', 'prepare_resume',
|
|
45
|
+
'train', 'resume', 'open_run', 'inspect', 'metrics', 'evaluations', 'observations', 'catalog', 'events',
|
|
46
|
+
'samples', 'sample', 'load_inference', 'view', 'evaluate', 'image', 'audio',
|
|
47
|
+
'lift', 'load_recipe', 'describe', 'describe_model', 'data_check', 'check_data', 'preflight', 'preflight_check', 'serve', 'viewer_status',
|
|
48
|
+
'stop_viewer', 'contributions', 'project', 'checkpoint_status', 'request_checkpoint',
|
|
49
|
+
'export', 'prune_checkpoints', 'resolve_checkpoint', 'extract_ema_bundle',
|
|
50
|
+
'ensure_dashboard_running', 'stop_all_servers', 'dashboard_status', 'stop_dashboard',
|
|
51
|
+
'find_runs_root', 'stop_projector',
|
|
52
|
+
'find_workspace', 'load_workspace', 'interpolate_variables', 'resolve_device_aliases',
|
|
53
|
+
'resolve_inheritance', 'collect_source_files',
|
|
54
|
+
'status', 'ps', 'stop', 'logs', 'tag', 'rm', 'list_runs', 'runs', 'runs_ls', 'runs_compare', 'compare', 'diff', 'gc', 'prune', 'gc_all', 'compact_events',
|
|
55
|
+
'resolve_run_target', 'register_run', 'sync_registry', 'generate_run_slug',
|
|
56
|
+
'queue_add', 'queue_ls', 'queue_status', 'queue_stop', 'queue_run', 'queue_install_service', 'queue_clear']
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------------------- model files
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Model:
|
|
65
|
+
"""A model file's contents. ``base`` resolves relative ``hndl`` paths."""
|
|
66
|
+
spec: dict
|
|
67
|
+
base: Path | None = None
|
|
68
|
+
path: Path | None = None
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def name(self):
|
|
72
|
+
return self.spec.get('name', 'custom/model')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _model(value):
|
|
76
|
+
if isinstance(value, Model):
|
|
77
|
+
return value
|
|
78
|
+
if isinstance(value, (str, Path)):
|
|
79
|
+
return load(value)
|
|
80
|
+
if isinstance(value, dict):
|
|
81
|
+
return from_dict(value)
|
|
82
|
+
raise TypeError(f'Expected a Model, model file path or dictionary, got {type(value).__name__}')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load(path):
|
|
86
|
+
"""Load a model file, an engine recipe TOML, a run's stored Model, or a .pt bundle."""
|
|
87
|
+
if isinstance(path, Run):
|
|
88
|
+
return _load_stored_run_model(path.path)
|
|
89
|
+
raw = str(path)
|
|
90
|
+
path = Path(path)
|
|
91
|
+
if path.suffix.lower() == '.pt':
|
|
92
|
+
return load_bundle(path)
|
|
93
|
+
if not path.exists() and path.suffix.lower() not in ('.toml', '.pt'):
|
|
94
|
+
try:
|
|
95
|
+
resolved_run = resolve_run_target(raw, allow_latest=(raw == 'latest'))
|
|
96
|
+
if resolved_run.is_dir():
|
|
97
|
+
return _load_stored_run_model(resolved_run)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
if path.is_dir():
|
|
101
|
+
if (path / 'model.toml').is_file():
|
|
102
|
+
path = path / 'model.toml'
|
|
103
|
+
elif (path / 'config.toml').is_file():
|
|
104
|
+
path = path / 'config.toml'
|
|
105
|
+
elif any(
|
|
106
|
+
(path / name).exists()
|
|
107
|
+
for name in ('recipe.toml', 'manifest.json', 'summary.json', 'config-source')
|
|
108
|
+
):
|
|
109
|
+
return _load_stored_run_model(path)
|
|
110
|
+
else:
|
|
111
|
+
path = path / 'config.toml'
|
|
112
|
+
with path.open('rb') as stream:
|
|
113
|
+
spec = tomllib.load(stream)
|
|
114
|
+
|
|
115
|
+
if isinstance(spec, dict):
|
|
116
|
+
ws_path = find_workspace(path)
|
|
117
|
+
ws_dict = load_workspace(ws_path) if ws_path else None
|
|
118
|
+
ws_root = ws_path.parent if ws_path else None
|
|
119
|
+
|
|
120
|
+
if 'extends' in spec:
|
|
121
|
+
spec = resolve_inheritance(spec, config_path=path, workspace_root=ws_root)
|
|
122
|
+
elif is_model_file(spec) and 'name' not in spec:
|
|
123
|
+
spec['name'] = path.stem
|
|
124
|
+
|
|
125
|
+
if ws_dict is not None:
|
|
126
|
+
spec = interpolate_variables(spec, ws_dict)
|
|
127
|
+
|
|
128
|
+
return Model(spec, base=path.parent.resolve(), path=path.resolve())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def packaged(name):
|
|
133
|
+
"""A model file shipped with HyperGAN, e.g. ``packaged("gaussian-grid")``."""
|
|
134
|
+
path = PACKAGED / f'{name}.toml'
|
|
135
|
+
if re.fullmatch(r'[a-z0-9][a-z0-9-]*', name) is None or not path.is_file():
|
|
136
|
+
choices = sorted(p.stem for p in PACKAGED.glob('*.toml'))
|
|
137
|
+
raise ValueError(f'Unknown packaged model {name!r}; choose one of {", ".join(choices)}')
|
|
138
|
+
return load(path)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def packaged_list():
|
|
142
|
+
"""List available packaged model files."""
|
|
143
|
+
results = []
|
|
144
|
+
for path in sorted(PACKAGED.glob('*.toml')):
|
|
145
|
+
try:
|
|
146
|
+
m = load(path)
|
|
147
|
+
results.append({
|
|
148
|
+
'name': path.stem,
|
|
149
|
+
'path': str(path),
|
|
150
|
+
'description': m.spec.get('name', path.stem),
|
|
151
|
+
'scope': 'numerical-reference' if path.stem == 'gaussian-grid' else 'packaged',
|
|
152
|
+
})
|
|
153
|
+
except Exception:
|
|
154
|
+
results.append({'name': path.stem, 'path': str(path)})
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def from_dict(spec, base=None):
|
|
159
|
+
"""A model built in Python; ``base`` resolves any relative ``hndl`` paths."""
|
|
160
|
+
if not isinstance(spec, dict):
|
|
161
|
+
raise TypeError('A model is a dictionary shaped like a model file')
|
|
162
|
+
return Model(deepcopy(spec), base=Path(base).resolve() if base is not None else None)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def model(name='custom/model', *, data=None, prior=None, networks=None, losses=None,
|
|
166
|
+
penalty=None, train=None, sampling=None, samplers=None, metrics=None,
|
|
167
|
+
evaluations=None, defaults=None, base=None, **kwargs):
|
|
168
|
+
"""Keyword constructor for a Model (front end over from_dict)."""
|
|
169
|
+
spec = {'format': kwargs.pop('format', FORMAT)}
|
|
170
|
+
if name is not None:
|
|
171
|
+
spec['name'] = name
|
|
172
|
+
if defaults is not None:
|
|
173
|
+
spec['defaults'] = defaults
|
|
174
|
+
if data is not None:
|
|
175
|
+
spec['data'] = data
|
|
176
|
+
if prior is not None:
|
|
177
|
+
spec['prior'] = prior
|
|
178
|
+
if networks is not None:
|
|
179
|
+
spec['networks'] = networks
|
|
180
|
+
if losses is not None:
|
|
181
|
+
spec['losses'] = losses
|
|
182
|
+
if penalty is not None:
|
|
183
|
+
spec['penalty'] = penalty
|
|
184
|
+
if train is not None:
|
|
185
|
+
spec['train'] = train
|
|
186
|
+
if sampling is not None:
|
|
187
|
+
spec['sampling'] = sampling
|
|
188
|
+
if samplers is not None:
|
|
189
|
+
spec['samplers'] = samplers
|
|
190
|
+
if metrics is not None:
|
|
191
|
+
spec['metrics'] = metrics
|
|
192
|
+
if evaluations is not None:
|
|
193
|
+
spec['evaluations'] = evaluations
|
|
194
|
+
spec.update(kwargs)
|
|
195
|
+
return from_dict(spec, base=base)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _segment(container, key, location):
|
|
199
|
+
if isinstance(container, list):
|
|
200
|
+
if key.isdigit() and int(key) < len(container):
|
|
201
|
+
return int(key)
|
|
202
|
+
# Losses, like other lists of tables, can be addressed by id, critic, or type.
|
|
203
|
+
for index, item in enumerate(container):
|
|
204
|
+
if isinstance(item, dict) and key in (item.get('id'), item.get('critic')):
|
|
205
|
+
return index
|
|
206
|
+
for index, item in enumerate(container):
|
|
207
|
+
if isinstance(item, dict) and key == item.get('type'):
|
|
208
|
+
return index
|
|
209
|
+
raise KeyError(f'{location}: no list element {key!r} (use an index, id, critic, or type name)')
|
|
210
|
+
return key
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def override(model, changes=None, /, **sections):
|
|
214
|
+
"""Return a copy with dotted-path values set, e.g. ``{"train.steps": 10}``.
|
|
215
|
+
|
|
216
|
+
List elements are addressed by index, ``id`` or ``critic``:
|
|
217
|
+
``"losses.reconstruction.weight"``. Keyword sections deep-merge tables:
|
|
218
|
+
``override(model, train={"steps": 10})``.
|
|
219
|
+
"""
|
|
220
|
+
from .model_file import OPTIMIZER_FIELDS, is_model_file
|
|
221
|
+
model = _model(model)
|
|
222
|
+
spec = deepcopy(model.spec)
|
|
223
|
+
is_model = is_model_file(spec)
|
|
224
|
+
|
|
225
|
+
norm_changes = {}
|
|
226
|
+
for dotted, value in (changes or {}).items():
|
|
227
|
+
if is_model and dotted in ('trainer', 'training', 'optimizer') and isinstance(value, dict):
|
|
228
|
+
for k, v in value.items():
|
|
229
|
+
norm_changes[f'train.{k}'] = v
|
|
230
|
+
continue
|
|
231
|
+
if not is_model and dotted in ('train', 'trainer') and isinstance(value, dict):
|
|
232
|
+
for k, v in value.items():
|
|
233
|
+
if k in OPTIMIZER_FIELDS and 'optimizer' in spec:
|
|
234
|
+
norm_changes[f'optimizer.{k}'] = v
|
|
235
|
+
else:
|
|
236
|
+
norm_changes[f'training.{k}'] = v
|
|
237
|
+
continue
|
|
238
|
+
if dotted.startswith('trainer.'):
|
|
239
|
+
dotted = 'train.' + dotted[len('trainer.'):]
|
|
240
|
+
if not is_model and (dotted.startswith('train.') or dotted.startswith('training.')):
|
|
241
|
+
field = dotted.split('.', 1)[1]
|
|
242
|
+
if field in OPTIMIZER_FIELDS and 'optimizer' in spec:
|
|
243
|
+
norm_changes[f'optimizer.{field}'] = value
|
|
244
|
+
elif 'training' in spec:
|
|
245
|
+
norm_changes[f'training.{field}'] = value
|
|
246
|
+
else:
|
|
247
|
+
norm_changes[dotted] = value
|
|
248
|
+
elif is_model and (dotted.startswith('training.') or dotted.startswith('optimizer.')):
|
|
249
|
+
field = dotted.split('.', 1)[1]
|
|
250
|
+
norm_changes[f'train.{field}'] = value
|
|
251
|
+
else:
|
|
252
|
+
norm_changes[dotted] = value
|
|
253
|
+
|
|
254
|
+
for dotted, value in norm_changes.items():
|
|
255
|
+
parts = dotted.split('.')
|
|
256
|
+
target = spec
|
|
257
|
+
for depth, part in enumerate(parts[:-1]):
|
|
258
|
+
key = _segment(target, part, '.'.join(parts[:depth + 1]))
|
|
259
|
+
if isinstance(target, dict) and key not in target:
|
|
260
|
+
target[key] = {}
|
|
261
|
+
target = target[key]
|
|
262
|
+
last_key = _segment(target, parts[-1], dotted) if isinstance(target, list) else parts[-1]
|
|
263
|
+
target[last_key] = deepcopy(value)
|
|
264
|
+
if isinstance(target, dict) and last_key == 'id' and value is None:
|
|
265
|
+
target['__id_required__'] = True
|
|
266
|
+
|
|
267
|
+
norm_sections = {}
|
|
268
|
+
for key, value in sections.items():
|
|
269
|
+
if is_model and key in ('trainer', 'training', 'optimizer') and isinstance(value, dict):
|
|
270
|
+
norm_sections.setdefault('train', {}).update(deepcopy(value))
|
|
271
|
+
else:
|
|
272
|
+
norm_sections[key] = value
|
|
273
|
+
|
|
274
|
+
def merge(into, update):
|
|
275
|
+
for key, value in update.items():
|
|
276
|
+
if isinstance(value, dict) and isinstance(into.get(key), dict):
|
|
277
|
+
merge(into[key], value)
|
|
278
|
+
else:
|
|
279
|
+
into[key] = deepcopy(value)
|
|
280
|
+
merge(spec, norm_sections)
|
|
281
|
+
return Model(spec, base=model.base, path=None)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _relocated(spec, base, destination):
|
|
285
|
+
"""Rewrite relative hndl paths so a saved copy still reads the same files."""
|
|
286
|
+
spec = deepcopy(spec)
|
|
287
|
+
def relocated(file):
|
|
288
|
+
if not isinstance(file, str) or Path(file).is_absolute():
|
|
289
|
+
return file
|
|
290
|
+
if base is None:
|
|
291
|
+
raise ModelFileError('A model with relative hndl paths needs a base directory to be saved elsewhere')
|
|
292
|
+
import os
|
|
293
|
+
source = (base / file).resolve()
|
|
294
|
+
try:
|
|
295
|
+
shared = Path(os.path.commonpath([source, destination]))
|
|
296
|
+
except ValueError:
|
|
297
|
+
# Windows volumes have no shared directory or relative path.
|
|
298
|
+
return source.as_posix()
|
|
299
|
+
# Relative when the files share a project directory, else absolute.
|
|
300
|
+
return (Path(os.path.relpath(source, destination)).as_posix()
|
|
301
|
+
if shared != Path(shared.anchor) else source.as_posix())
|
|
302
|
+
|
|
303
|
+
for network in (spec.get('networks') or {}).values():
|
|
304
|
+
if isinstance(network, dict):
|
|
305
|
+
for hndl_key in ('hndl', 'file'):
|
|
306
|
+
if network.get(hndl_key) is not None:
|
|
307
|
+
network[hndl_key] = relocated(network[hndl_key])
|
|
308
|
+
for name, file in (network.get('network_files') or {}).items():
|
|
309
|
+
network['network_files'][name] = relocated(file)
|
|
310
|
+
for component in (spec.get('components') or {}).values():
|
|
311
|
+
args = component.get('args', {})
|
|
312
|
+
if component.get('factory') == 'hndl' and 'file' in args:
|
|
313
|
+
args['file'] = relocated(args['file'])
|
|
314
|
+
for name, file in args.get('network_files', {}).items():
|
|
315
|
+
args['network_files'][name] = relocated(file)
|
|
316
|
+
return spec
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def dumps(model):
|
|
320
|
+
"""The model file text (TOML)."""
|
|
321
|
+
model = _model(model)
|
|
322
|
+
return _dumps(model.spec, table_depth=3)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
_HASH = re.compile(r"^# body-sha256: ([0-9a-f]{64})$", re.M)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def save(model, path, *, overwrite=False):
|
|
329
|
+
"""Write the model file; referenced .hndl files and Python are not copied."""
|
|
330
|
+
model = _model(model)
|
|
331
|
+
path = Path(path)
|
|
332
|
+
if path.suffix != '.toml':
|
|
333
|
+
raise ValueError('Model files use the .toml suffix')
|
|
334
|
+
spec = _relocated(model.spec, model.base, path.parent.resolve())
|
|
335
|
+
if is_model_file(spec):
|
|
336
|
+
spec = {'format': FORMAT, **{k: v for k, v in spec.items() if k != 'format'}}
|
|
337
|
+
body = _dumps(spec, table_depth=3)
|
|
338
|
+
digest = hashlib.sha256(body.encode()).hexdigest()
|
|
339
|
+
header = (
|
|
340
|
+
"# HyperGAN model written by hypergan.api (hg.save). Train it with `hypergan train` or hg.train.\n"
|
|
341
|
+
"# Networks and data are referenced by import path; edit those modules, not copies.\n"
|
|
342
|
+
"# hg.save replaces this file only while it is unedited; after editing it here, load it with hg.load.\n"
|
|
343
|
+
f"# body-sha256: {digest}\n\n"
|
|
344
|
+
)
|
|
345
|
+
text = header + body
|
|
346
|
+
if path.exists() and not overwrite:
|
|
347
|
+
existing = path.read_text(encoding="utf-8")
|
|
348
|
+
match = _HASH.search(existing)
|
|
349
|
+
if existing != text:
|
|
350
|
+
if match is None:
|
|
351
|
+
raise FileExistsError(f"{path} exists and was not written by hg.save; pass overwrite=True or choose another path")
|
|
352
|
+
current = existing.split("\n\n", 1)[1] if "\n\n" in existing else ""
|
|
353
|
+
if hashlib.sha256(current.encode()).hexdigest() != match.group(1):
|
|
354
|
+
raise FileExistsError(f"{path} was edited after hg.save wrote it; load it with hg.load(path) "
|
|
355
|
+
"to train the edited version, or pass overwrite=True to replace it")
|
|
356
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
357
|
+
temporary = path.with_name(path.name + '.tmp')
|
|
358
|
+
temporary.write_text(text, encoding="utf-8")
|
|
359
|
+
temporary.replace(path)
|
|
360
|
+
return path
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def lower(model):
|
|
364
|
+
"""The engine recipe for this model (torch-free, no user code imported)."""
|
|
365
|
+
model = _model(model)
|
|
366
|
+
if is_model_file(model.spec):
|
|
367
|
+
return _lower(model.spec, model.base)
|
|
368
|
+
if model.base is not None:
|
|
369
|
+
from .config import inline_network_sources
|
|
370
|
+
return inline_network_sources(model.spec, model.base)
|
|
371
|
+
return deepcopy(model.spec)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def resolve(model):
|
|
375
|
+
"""The complete resolved configuration a run would record."""
|
|
376
|
+
from .config import load_config
|
|
377
|
+
return load_config(lower(model))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def validate(model):
|
|
381
|
+
"""Check the model without Torch or user code; return warnings, raise on errors."""
|
|
382
|
+
model = _model(model)
|
|
383
|
+
resolved = resolve(model)
|
|
384
|
+
extra = model_warnings(model.spec) if is_model_file(model.spec) else []
|
|
385
|
+
return extra + list(resolved['warnings'])
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
validate_config = validate
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def init_project(project_dir, *, template="dcgan", resolution=None, data_dir=None, output_config=None, device="cuda", overwrite=False):
|
|
392
|
+
"""Initialize a HyperGAN project directory with hypergan.toml, runs/, and configs/<template>.toml."""
|
|
393
|
+
from .recipe_generator import init_project as _init_project
|
|
394
|
+
return _init_project(project_dir, template=template, resolution=resolution, data_dir=data_dir,
|
|
395
|
+
output_config=output_config, device=device, overwrite=overwrite)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def new(data_dir=None, output_path=None, *, template=None, resolution=None, device="cuda",
|
|
399
|
+
project=None, overwrite=False, **kwargs):
|
|
400
|
+
"""Scaffold a new HyperGAN model file (or initialize a project directory when project is set)."""
|
|
401
|
+
if project is not None:
|
|
402
|
+
return init_project(
|
|
403
|
+
project,
|
|
404
|
+
template=template or "dcgan",
|
|
405
|
+
resolution=resolution,
|
|
406
|
+
data_dir=data_dir,
|
|
407
|
+
output_config=output_path,
|
|
408
|
+
device=device,
|
|
409
|
+
overwrite=overwrite,
|
|
410
|
+
)
|
|
411
|
+
from .recipe_generator import generate_recipe as _generate_recipe
|
|
412
|
+
ds_dir = data_dir
|
|
413
|
+
out_p = output_path
|
|
414
|
+
if out_p is None and ds_dir is not None:
|
|
415
|
+
p = Path(ds_dir)
|
|
416
|
+
if p.suffix.lower() == ".toml" or not p.is_dir():
|
|
417
|
+
out_p = ds_dir
|
|
418
|
+
ds_dir = None
|
|
419
|
+
name_arg = str(out_p) if out_p is not None else kwargs.pop("name", None)
|
|
420
|
+
return _generate_recipe(
|
|
421
|
+
name=name_arg,
|
|
422
|
+
template=template or "dcgan",
|
|
423
|
+
resolution=resolution,
|
|
424
|
+
dataset_dir=ds_dir,
|
|
425
|
+
device=device,
|
|
426
|
+
overwrite=overwrite,
|
|
427
|
+
**kwargs,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def fingerprint(model):
|
|
432
|
+
"""Numerical identity: equal fingerprints train the same recipe."""
|
|
433
|
+
from .config import fingerprint as recipe_fingerprint
|
|
434
|
+
return recipe_fingerprint(resolve(model))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def explain(model):
|
|
438
|
+
"""Per network: role, optimizer group, own losses, and gradient that reaches it."""
|
|
439
|
+
model = _model(model)
|
|
440
|
+
recipe = lower(model)
|
|
441
|
+
components = recipe.get('components', {})
|
|
442
|
+
name_map = recipe.get('name_map', {})
|
|
443
|
+
inv_name_map = {v: k for k, v in name_map.items()}
|
|
444
|
+
opt_cfg = recipe.get('optimizer', {})
|
|
445
|
+
rec_cfg = recipe.get('recipe', {})
|
|
446
|
+
eff_lr = opt_cfg.get('lr', rec_cfg.get('lr', 0.0002))
|
|
447
|
+
eff_d_mult = opt_cfg.get('d_lr_mult', rec_cfg.get('d_lr_mult', 1.0))
|
|
448
|
+
eff_d_lr = eff_lr * eff_d_mult if isinstance(eff_lr, (int, float)) and isinstance(eff_d_mult, (int, float)) else None
|
|
449
|
+
g_lr_str = f'optimizer.lr ({eff_lr:g})' if isinstance(eff_lr, (int, float)) else 'optimizer.lr'
|
|
450
|
+
d_lr_str = f'optimizer.lr * d_lr_mult ({eff_d_lr:g})' if isinstance(eff_d_lr, (int, float)) else 'optimizer.lr * d_lr_mult'
|
|
451
|
+
reach = {name: [] for name in components}
|
|
452
|
+
for term in recipe.get('objectives', []):
|
|
453
|
+
detached = set(term.get('detach', ['target'] if 'target' in term.get('inputs', {}) else []))
|
|
454
|
+
seen = set()
|
|
455
|
+
for key, path in term.get('inputs', {}).items():
|
|
456
|
+
if key not in detached:
|
|
457
|
+
upstream(path, components, seen=seen)
|
|
458
|
+
label = term.get('id') or term.get('factory')
|
|
459
|
+
for name in seen:
|
|
460
|
+
reach[name].append(f'loss {label}')
|
|
461
|
+
terms = []
|
|
462
|
+
if 'adversarial' in recipe:
|
|
463
|
+
disc_inputs = components.get('discriminator', {}).get('inputs', {})
|
|
464
|
+
fake_path = recipe['adversarial'].get('fake', 'generated')
|
|
465
|
+
if any(p == 'candidate' or p.startswith('candidate.') or producer(p) == 'generator' for p in disc_inputs.values()):
|
|
466
|
+
terms.append(('discriminator', fake_path))
|
|
467
|
+
for t in recipe.get('adversarial_terms', ()):
|
|
468
|
+
terms.append((t['component'], t['fake']))
|
|
469
|
+
for critic_name, fake in terms:
|
|
470
|
+
orig_critic = inv_name_map.get(critic_name, critic_name)
|
|
471
|
+
critic_label = f'{critic_name} ({orig_critic})' if orig_critic != critic_name else critic_name
|
|
472
|
+
for name in upstream(fake, components):
|
|
473
|
+
reach[name].append(f'fooling {critic_label}')
|
|
474
|
+
|
|
475
|
+
lines = []
|
|
476
|
+
networks = model.spec.get('networks') if is_model_file(model.spec) else None
|
|
477
|
+
if networks:
|
|
478
|
+
for orig_name, net_spec in networks.items():
|
|
479
|
+
comp_name = name_map.get(orig_name, orig_name)
|
|
480
|
+
role = net_spec.get('role', 'generator')
|
|
481
|
+
spec = components.get(comp_name, {})
|
|
482
|
+
factory = spec.get('factory', net_spec.get('hndl') or net_spec.get('file') or net_spec.get('module') or 'unknown')
|
|
483
|
+
if role == 'critic':
|
|
484
|
+
group = f'critic Adam (shared by all critics), lr = {d_lr_str}'
|
|
485
|
+
elif 'reuse' in net_spec or 'reuse' in spec:
|
|
486
|
+
reuse_target = net_spec.get('reuse') or spec.get('reuse')
|
|
487
|
+
group = f'parameters of {reuse_target}'
|
|
488
|
+
elif net_spec.get('trainable') is False or spec.get('trainable') is False or net_spec.get('freeze_parameters') or spec.get('freeze_parameters') or role == 'frozen':
|
|
489
|
+
group = 'none (frozen)'
|
|
490
|
+
elif role == 'shared':
|
|
491
|
+
group = f'parameters of {spec.get("reuse")}'
|
|
492
|
+
else:
|
|
493
|
+
group = f'generator-side Adam, lr = {g_lr_str}'
|
|
494
|
+
lines.append(f'{orig_name} [{role}] {factory}')
|
|
495
|
+
inputs_str = ', '.join(f'{k} <- {v}' for k, v in net_spec.get('inputs', {}).items())
|
|
496
|
+
lines.append(f' inputs: {inputs_str}')
|
|
497
|
+
lines.append(f' optimizer: {group}')
|
|
498
|
+
if role != 'critic':
|
|
499
|
+
received = sorted(set(reach.get(comp_name, [])))
|
|
500
|
+
lines.append(' gradient from: ' + (', '.join(received) or '-'))
|
|
501
|
+
else:
|
|
502
|
+
for name, spec in components.items():
|
|
503
|
+
orig_name = inv_name_map.get(name, name)
|
|
504
|
+
factory = spec.get('factory', 'reuse')
|
|
505
|
+
role = 'critic' if name == 'discriminator' or any(t.get('component') == name for t in recipe.get('adversarial_terms', [])) else 'generator'
|
|
506
|
+
if role == 'critic':
|
|
507
|
+
group = f'critic Adam (shared by all critics), lr = {d_lr_str}'
|
|
508
|
+
else:
|
|
509
|
+
group = f'generator-side Adam, lr = {g_lr_str}'
|
|
510
|
+
lines.append(f'{orig_name} [{role}] {factory}')
|
|
511
|
+
inputs_str = ', '.join(f'{k} <- {v}' for k, v in spec.get('inputs', {}).items())
|
|
512
|
+
lines.append(f' inputs: {inputs_str}')
|
|
513
|
+
lines.append(f' optimizer: {group}')
|
|
514
|
+
received = sorted(set(reach.get(name, [])))
|
|
515
|
+
lines.append(' gradient from: ' + (', '.join(received) or '-'))
|
|
516
|
+
return '\n'.join(lines)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# --------------------------------------------------------------------------- building in Python
|
|
520
|
+
|
|
521
|
+
def plain(value, location):
|
|
522
|
+
"""Check that an argument survives the config file unchanged (no None, NaN or objects)."""
|
|
523
|
+
if value is None:
|
|
524
|
+
raise ValueError(f"{location} is None; the config file has no null, so omit the argument instead")
|
|
525
|
+
if isinstance(value, bool) or isinstance(value, (int, str)):
|
|
526
|
+
return value
|
|
527
|
+
if isinstance(value, float):
|
|
528
|
+
import math
|
|
529
|
+
if not math.isfinite(value):
|
|
530
|
+
raise ValueError(f"{location} must be finite")
|
|
531
|
+
return value
|
|
532
|
+
if isinstance(value, (list, tuple)):
|
|
533
|
+
return [plain(v, f"{location}[{i}]") for i, v in enumerate(value)]
|
|
534
|
+
if isinstance(value, dict):
|
|
535
|
+
if not all(isinstance(k, str) for k in value):
|
|
536
|
+
raise ValueError(f"{location} keys must be strings")
|
|
537
|
+
return {k: plain(v, f"{location}.{k}") for k, v in value.items()}
|
|
538
|
+
raise TypeError(f"{location} is a {type(value).__name__}; config arguments must be numbers, strings, lists or dicts")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def import_path(obj, what):
|
|
542
|
+
"""``module:qualname`` for a class or function that other processes can import."""
|
|
543
|
+
if isinstance(obj, str):
|
|
544
|
+
if obj.count(":") != 1 or not all(obj.split(":")):
|
|
545
|
+
raise ValueError(f"{what} must be an importable object or a 'module:object' string, got {obj!r}")
|
|
546
|
+
return obj
|
|
547
|
+
module_name, qualname = getattr(obj, "__module__", None), getattr(obj, "__qualname__", None)
|
|
548
|
+
if not module_name or not qualname:
|
|
549
|
+
raise TypeError(f"{what} must be a class or function, got {type(obj).__name__}")
|
|
550
|
+
if module_name == "__main__":
|
|
551
|
+
raise ValueError(
|
|
552
|
+
f"{what} {qualname} is defined in the script being run (__main__). Every training process "
|
|
553
|
+
f"rebuilds the model from the config file by import path, so move {qualname} into an "
|
|
554
|
+
f"importable module (for example my_project/networks.py) and import it from there."
|
|
555
|
+
)
|
|
556
|
+
if "<locals>" in qualname or "<lambda>" in qualname:
|
|
557
|
+
raise ValueError(f"{what} {qualname} is local to a function; define it at module level so it can be imported")
|
|
558
|
+
return f"{module_name}:{qualname}"
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def hndl(source=None, *, file=None, input_shape, output_shape, inputs, role, **args):
|
|
562
|
+
"""A network written in HNDL, inline (``source``) or in a file (``file``)."""
|
|
563
|
+
if (source is None) == (file is None):
|
|
564
|
+
raise ValueError('Pass exactly one of source or file')
|
|
565
|
+
network = {'role': role, 'inputs': dict(inputs), 'input_shape': input_shape, 'output_shape': output_shape}
|
|
566
|
+
network.update({'source': source} if source is not None else {'hndl': str(file)})
|
|
567
|
+
if args:
|
|
568
|
+
network['args'] = args
|
|
569
|
+
return network
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def module(path, *, inputs, role, trainable=None, **args):
|
|
573
|
+
"""A network implemented as an importable ``torch.nn.Module`` class or factory."""
|
|
574
|
+
path_str = import_path(path, 'Network') if callable(path) else path
|
|
575
|
+
if isinstance(path_str, str) and (path_str.count(':') != 1 or not all(path_str.split(':'))):
|
|
576
|
+
raise ValueError(f"Network must be an importable object or a 'module:object' string, got {path_str!r}")
|
|
577
|
+
if callable(path):
|
|
578
|
+
try:
|
|
579
|
+
sig = _py_inspect.signature(path)
|
|
580
|
+
if not any(p.kind == _py_inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
|
|
581
|
+
for arg_name in args:
|
|
582
|
+
if arg_name not in sig.parameters:
|
|
583
|
+
raise TypeError(f"Network {getattr(path, '__name__', path)} got unexpected keyword argument {arg_name!r}")
|
|
584
|
+
except (ValueError, TypeError) as exc:
|
|
585
|
+
if "unexpected keyword argument" in str(exc):
|
|
586
|
+
raise
|
|
587
|
+
network = {'role': role, 'module': path_str, 'inputs': dict(inputs)}
|
|
588
|
+
if args:
|
|
589
|
+
network['args'] = args
|
|
590
|
+
if trainable is not None:
|
|
591
|
+
network['trainable'] = trainable
|
|
592
|
+
return network
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def sampler(fn, *, inputs=None, count=None, seed=None, **args):
|
|
596
|
+
"""Declare a sampler entry for [samplers.<name>]."""
|
|
597
|
+
fn_str = import_path(fn, 'Sampler') if callable(fn) else fn
|
|
598
|
+
entry = {'fn': fn_str}
|
|
599
|
+
if inputs is not None:
|
|
600
|
+
entry['inputs'] = dict(inputs)
|
|
601
|
+
if count is not None:
|
|
602
|
+
entry['count'] = count
|
|
603
|
+
if seed is not None:
|
|
604
|
+
entry['seed'] = seed
|
|
605
|
+
if args:
|
|
606
|
+
entry['args'] = args
|
|
607
|
+
return entry
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def adversarial(critic, *, weight=1.0, penalty=1.0, real=None, fake=None, inputs=None, id=None):
|
|
611
|
+
"""Define an adversarial loss term binding a critic network to real and generated inputs."""
|
|
612
|
+
loss = {'type': 'adversarial', 'critic': critic, 'weight': weight, 'penalty': penalty}
|
|
613
|
+
for key, value in (('real', real), ('fake', fake), ('inputs', inputs), ('id', id)):
|
|
614
|
+
if value is not None:
|
|
615
|
+
loss[key] = value
|
|
616
|
+
return loss
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def objective(id, fn='mse', *, input=None, target=None, inputs=None, weight=1.0, args=None, type='objective'):
|
|
620
|
+
"""Define a loss objective term computing a loss function over specified input tensors."""
|
|
621
|
+
if callable(fn):
|
|
622
|
+
fn_str = import_path(fn, f'Objective {id}')
|
|
623
|
+
else:
|
|
624
|
+
fn_str = fn
|
|
625
|
+
loss = {'type': type, 'id': id, 'fn': fn_str, 'weight': weight}
|
|
626
|
+
if inputs is not None:
|
|
627
|
+
loss['inputs'] = inputs
|
|
628
|
+
else:
|
|
629
|
+
loss['input'] = input
|
|
630
|
+
if target is not None:
|
|
631
|
+
loss['target'] = target
|
|
632
|
+
if args:
|
|
633
|
+
loss['args'] = args
|
|
634
|
+
return loss
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def reconstruction(id='reconstruction', fn='mse', *, input='generated', target='batch.real', weight=1.0):
|
|
638
|
+
"""Define a reconstruction loss term between generated output and a target tensor (default MSE)."""
|
|
639
|
+
return objective(id, fn, input=input, target=target, weight=weight, type='reconstruction')
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def prior_loss(weight=1.0, **settings):
|
|
643
|
+
"""Define a prior regularization loss term with an optional weight and settings."""
|
|
644
|
+
return {'type': 'prior', 'weight': weight, **settings}
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# --------------------------------------------------------------------------- training
|
|
648
|
+
|
|
649
|
+
@dataclass(frozen=True)
|
|
650
|
+
class Run:
|
|
651
|
+
"""A run directory and the manifest it recorded."""
|
|
652
|
+
path: Path
|
|
653
|
+
manifest: dict = field(repr=False)
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def status(self):
|
|
657
|
+
"""Current lifecycle status of the run (running, completed, stopped, etc.)."""
|
|
658
|
+
return self.manifest.get('status')
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def step(self):
|
|
662
|
+
"""Latest recorded training step count."""
|
|
663
|
+
return self.manifest.get('steps')
|
|
664
|
+
|
|
665
|
+
@property
|
|
666
|
+
def config(self):
|
|
667
|
+
"""Resolved configuration recorded in the run manifest."""
|
|
668
|
+
return self.manifest.get('config')
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def fingerprint(self):
|
|
672
|
+
"""Numerical recipe fingerprint (config_sha256)."""
|
|
673
|
+
return self.manifest.get('config_sha256')
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def run_id(self) -> str:
|
|
677
|
+
"""Unique 32-character hexadecimal run identifier."""
|
|
678
|
+
rid = self.manifest.get('run_id')
|
|
679
|
+
if rid:
|
|
680
|
+
return str(rid)
|
|
681
|
+
summary_p = self.path / 'summary.json'
|
|
682
|
+
if summary_p.is_file():
|
|
683
|
+
try:
|
|
684
|
+
data = json.loads(summary_p.read_text(encoding='utf-8'))
|
|
685
|
+
if isinstance(data, dict) and data.get('run_id'):
|
|
686
|
+
return str(data['run_id'])
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
689
|
+
try:
|
|
690
|
+
from .registry import _read_registry_unlocked, get_registry_dir
|
|
691
|
+
reg_data = _read_registry_unlocked(get_registry_dir(self.path))
|
|
692
|
+
entry = reg_data.get('runs', {}).get(str(self.path.resolve()))
|
|
693
|
+
if entry and entry.get('run_id'):
|
|
694
|
+
return str(entry['run_id'])
|
|
695
|
+
except Exception:
|
|
696
|
+
pass
|
|
697
|
+
return self.path.name
|
|
698
|
+
|
|
699
|
+
@property
|
|
700
|
+
def short_id(self) -> str:
|
|
701
|
+
"""First 8 characters of the run identifier."""
|
|
702
|
+
sid = self.manifest.get('short_id')
|
|
703
|
+
if sid:
|
|
704
|
+
return str(sid)
|
|
705
|
+
summary_p = self.path / 'summary.json'
|
|
706
|
+
if summary_p.is_file():
|
|
707
|
+
try:
|
|
708
|
+
data = json.loads(summary_p.read_text(encoding='utf-8'))
|
|
709
|
+
if isinstance(data, dict) and data.get('short_id'):
|
|
710
|
+
return str(data['short_id'])
|
|
711
|
+
except Exception:
|
|
712
|
+
pass
|
|
713
|
+
return str(self.run_id)[:8]
|
|
714
|
+
|
|
715
|
+
@property
|
|
716
|
+
def name(self) -> str:
|
|
717
|
+
"""Human-readable experiment name or custom tag."""
|
|
718
|
+
nm = self.manifest.get('name') or (self.manifest.get('config') or {}).get('name')
|
|
719
|
+
if nm:
|
|
720
|
+
return str(nm)
|
|
721
|
+
summary_p = self.path / 'summary.json'
|
|
722
|
+
if summary_p.is_file():
|
|
723
|
+
try:
|
|
724
|
+
data = json.loads(summary_p.read_text(encoding='utf-8'))
|
|
725
|
+
if isinstance(data, dict) and data.get('name'):
|
|
726
|
+
return str(data['name'])
|
|
727
|
+
except Exception:
|
|
728
|
+
pass
|
|
729
|
+
try:
|
|
730
|
+
from .registry import _read_registry_unlocked, get_registry_dir
|
|
731
|
+
reg_data = _read_registry_unlocked(get_registry_dir(self.path))
|
|
732
|
+
entry = reg_data.get('runs', {}).get(str(self.path.resolve()))
|
|
733
|
+
if entry and entry.get('name'):
|
|
734
|
+
return str(entry['name'])
|
|
735
|
+
except Exception:
|
|
736
|
+
pass
|
|
737
|
+
return self.path.name
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def slug(self) -> str:
|
|
741
|
+
"""Memorable '<adj>-<noun>' run slug."""
|
|
742
|
+
sl = self.manifest.get('slug')
|
|
743
|
+
if sl:
|
|
744
|
+
return str(sl)
|
|
745
|
+
summary_p = self.path / 'summary.json'
|
|
746
|
+
if summary_p.is_file():
|
|
747
|
+
try:
|
|
748
|
+
data = json.loads(summary_p.read_text(encoding='utf-8'))
|
|
749
|
+
if isinstance(data, dict) and data.get('slug'):
|
|
750
|
+
return str(data['slug'])
|
|
751
|
+
except Exception:
|
|
752
|
+
pass
|
|
753
|
+
try:
|
|
754
|
+
from .registry import _read_registry_unlocked, get_registry_dir
|
|
755
|
+
reg_data = _read_registry_unlocked(get_registry_dir(self.path))
|
|
756
|
+
entry = reg_data.get('runs', {}).get(str(self.path.resolve()))
|
|
757
|
+
if entry and entry.get('slug'):
|
|
758
|
+
return str(entry['slug'])
|
|
759
|
+
except Exception:
|
|
760
|
+
pass
|
|
761
|
+
from .registry import extract_or_derive_slug
|
|
762
|
+
return extract_or_derive_slug(str(self.run_id), name=str(self.name), run_dir_name=self.path.name)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def open_run(run_dir=None):
|
|
766
|
+
"""Open and load a Run instance and its manifest by directory, slug, tag, or short ID."""
|
|
767
|
+
if run_dir is not None and Path(run_dir).suffix == ".json":
|
|
768
|
+
manifest_path = Path(run_dir).resolve()
|
|
769
|
+
run_path = manifest_path.parent
|
|
770
|
+
else:
|
|
771
|
+
from .registry import resolve_run_target
|
|
772
|
+
run_path = resolve_run_target(run_dir)
|
|
773
|
+
if run_path.is_file():
|
|
774
|
+
manifest_path = run_path
|
|
775
|
+
run_path = run_path.parent
|
|
776
|
+
else:
|
|
777
|
+
manifest_path = run_path / 'manifest.json'
|
|
778
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
779
|
+
if not isinstance(manifest, dict):
|
|
780
|
+
raise ValueError("run manifest must contain a JSON object")
|
|
781
|
+
return Run(run_path, manifest)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _run(value):
|
|
785
|
+
return value if isinstance(value, Run) else open_run(value)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _run_path(value=None):
|
|
789
|
+
if isinstance(value, Run):
|
|
790
|
+
return value.path
|
|
791
|
+
from .registry import resolve_run_target
|
|
792
|
+
return resolve_run_target(value)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _default_run_dir(model_obj):
|
|
796
|
+
"""Determine default run directory <runs_root>/<slug> for a Model or path."""
|
|
797
|
+
cfg_path = getattr(model_obj, "path", None)
|
|
798
|
+
ws_path = find_workspace(cfg_path or Path.cwd())
|
|
799
|
+
if ws_path:
|
|
800
|
+
ws_data = load_workspace(ws_path)
|
|
801
|
+
ws_sec = ws_data.get("workspace", {})
|
|
802
|
+
runs_root = Path(ws_sec["runs_root"]).resolve() if "runs_root" in ws_sec else (ws_path.parent / "runs").resolve()
|
|
803
|
+
stem = cfg_path.stem if cfg_path else (model_obj.name.split("/")[-1] if model_obj.name else "run")
|
|
804
|
+
custom_name = model_obj.spec.get("name") if (model_obj.spec.get("name") and model_obj.spec.get("name") != "custom/model") else None
|
|
805
|
+
slug_dir_name, short_slug = (custom_name, custom_name) if custom_name else generate_run_slug(stem, parent_dir=runs_root)
|
|
806
|
+
if "run_dir" in ws_sec:
|
|
807
|
+
run_dir_str = interpolate_variables(
|
|
808
|
+
ws_sec["run_dir"],
|
|
809
|
+
ws_data,
|
|
810
|
+
{"stem": custom_name or stem, "name": slug_dir_name, "slug": short_slug},
|
|
811
|
+
)
|
|
812
|
+
return Path(run_dir_str).resolve()
|
|
813
|
+
return (runs_root / slug_dir_name).resolve()
|
|
814
|
+
from .web_autostart import find_runs_root as autostart_find
|
|
815
|
+
runs_root = autostart_find(Path.cwd())
|
|
816
|
+
if not runs_root.name == "runs" and (runs_root / "runs").is_dir():
|
|
817
|
+
runs_root = (runs_root / "runs").resolve()
|
|
818
|
+
stem = cfg_path.stem if cfg_path else (model_obj.name.split("/")[-1] if model_obj.name else "run")
|
|
819
|
+
custom_name = model_obj.spec.get("name") if (model_obj.spec.get("name") and model_obj.spec.get("name") != "custom/model") else None
|
|
820
|
+
slug_dir_name, short_slug = (custom_name, custom_name) if custom_name else generate_run_slug(stem, parent_dir=runs_root)
|
|
821
|
+
return (runs_root / slug_dir_name).resolve()
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def prepare(model, run_dir=None, steps=None, *, profile=None, service_policy=None,
|
|
825
|
+
devices=None,
|
|
826
|
+
checkpoint_every=None, max_seconds=None, stop_after_steps=None,
|
|
827
|
+
preview_every=None, previews=None, preview_keep=None, preview_keep_source=None,
|
|
828
|
+
preview_name=None, **extra):
|
|
829
|
+
"""Validate and prepare a training run, returning a PreparedExecution."""
|
|
830
|
+
from .execution import prepare_train as _prepare_train
|
|
831
|
+
model = _model(model)
|
|
832
|
+
if run_dir is None:
|
|
833
|
+
run_dir = _default_run_dir(model)
|
|
834
|
+
pe = preview_every if preview_every is not None else previews
|
|
835
|
+
lowered = lower(model)
|
|
836
|
+
if getattr(model, 'path', None):
|
|
837
|
+
lowered['_source_path'] = str(model.path)
|
|
838
|
+
return _prepare_train(lowered, run_dir, steps=steps, profile=profile,
|
|
839
|
+
service_policy=service_policy, devices=devices,
|
|
840
|
+
checkpoint_every=checkpoint_every,
|
|
841
|
+
max_seconds=max_seconds, stop_after_steps=stop_after_steps,
|
|
842
|
+
preview_every=pe, preview_keep=preview_keep,
|
|
843
|
+
preview_keep_source=preview_keep_source, preview_name=preview_name,
|
|
844
|
+
**extra)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _load_stored_run_model(run_path: Path):
|
|
848
|
+
"""Load a run's stored Model from recipe.toml, model.toml, config-source/*.toml, or manifest.json."""
|
|
849
|
+
if (run_path / 'model.toml').is_file():
|
|
850
|
+
return load(run_path / 'model.toml')
|
|
851
|
+
if (run_path / 'recipe.toml').is_file():
|
|
852
|
+
return load(run_path / 'recipe.toml')
|
|
853
|
+
if (run_path / 'config.toml').is_file():
|
|
854
|
+
return load(run_path / 'config.toml')
|
|
855
|
+
cs_dir = run_path / 'config-source'
|
|
856
|
+
if cs_dir.is_dir():
|
|
857
|
+
cs_manifest = cs_dir / 'manifest.json'
|
|
858
|
+
if cs_manifest.is_file():
|
|
859
|
+
try:
|
|
860
|
+
cs_data = json.loads(cs_manifest.read_text(encoding='utf-8'))
|
|
861
|
+
files = cs_data.get('files') or []
|
|
862
|
+
for f_entry in files:
|
|
863
|
+
if isinstance(f_entry, dict) and f_entry.get('snapshot'):
|
|
864
|
+
snap = cs_dir / f_entry['snapshot']
|
|
865
|
+
if snap.is_file() and snap.name != 'hypergan.toml':
|
|
866
|
+
return load(snap)
|
|
867
|
+
except Exception:
|
|
868
|
+
pass
|
|
869
|
+
tomls = [p for p in sorted(cs_dir.rglob('*.toml')) if p.name != 'hypergan.toml']
|
|
870
|
+
if tomls:
|
|
871
|
+
return load(tomls[0])
|
|
872
|
+
if (run_path / 'manifest.json').is_file():
|
|
873
|
+
with (run_path / 'manifest.json').open('rb') as f:
|
|
874
|
+
manifest = json.load(f)
|
|
875
|
+
saved_config = manifest.get('config')
|
|
876
|
+
if not isinstance(saved_config, dict):
|
|
877
|
+
raise ValueError(f"Run {run_path} has no recorded configuration in manifest.json")
|
|
878
|
+
if 'components' in saved_config and 'networks' not in saved_config:
|
|
879
|
+
try:
|
|
880
|
+
return lift(saved_config, base=run_path)
|
|
881
|
+
except Exception:
|
|
882
|
+
pass
|
|
883
|
+
return from_dict(saved_config, base=run_path)
|
|
884
|
+
raise ValueError(f"Run directory {run_path} does not exist or has no configuration")
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def prepare_resume(run_dir, checkpoint=None, model=None, *, steps=None,
|
|
888
|
+
profile=None, service_policy=None, devices=None,
|
|
889
|
+
checkpoint_every=None, max_seconds=None, stop_after_steps=None,
|
|
890
|
+
preview_every=None, previews=None, preview_keep=None, preview_keep_source=None,
|
|
891
|
+
preview_name=None, sets=None, overrides=None, **extra):
|
|
892
|
+
"""Validate and prepare a run resumption, returning a PreparedExecution."""
|
|
893
|
+
from .execution import prepare_resume as _prepare_resume
|
|
894
|
+
run_path = _run_path(run_dir)
|
|
895
|
+
merged_sets = dict(sets or {})
|
|
896
|
+
if overrides:
|
|
897
|
+
merged_sets.update(overrides)
|
|
898
|
+
sets = merged_sets or None
|
|
899
|
+
if model is None and (sets or steps is not None):
|
|
900
|
+
model = _load_stored_run_model(run_path)
|
|
901
|
+
if sets:
|
|
902
|
+
normalized_sets = {}
|
|
903
|
+
for k, v in sets.items():
|
|
904
|
+
if k.startswith('trainer.'):
|
|
905
|
+
k = 'train.' + k[len('trainer.'):]
|
|
906
|
+
if k.startswith('training.') and hasattr(model, 'spec') and 'train' in model.spec:
|
|
907
|
+
normalized_sets['train.' + k[len('training.'):]] = v
|
|
908
|
+
elif k.startswith('train.') and hasattr(model, 'spec') and 'training' in model.spec and 'train' not in model.spec:
|
|
909
|
+
normalized_sets['training.' + k[len('train.'):]] = v
|
|
910
|
+
else:
|
|
911
|
+
normalized_sets[k] = v
|
|
912
|
+
model = override(model, normalized_sets)
|
|
913
|
+
if steps is None:
|
|
914
|
+
step_val = normalized_sets.get('train.steps', normalized_sets.get('training.steps'))
|
|
915
|
+
if step_val is not None:
|
|
916
|
+
steps = int(step_val)
|
|
917
|
+
|
|
918
|
+
config_path = lower(model) if model is not None else None
|
|
919
|
+
pe = preview_every if preview_every is not None else previews
|
|
920
|
+
return _prepare_resume(run_path, checkpoint=checkpoint, config_path=config_path, steps=steps,
|
|
921
|
+
profile=profile, service_policy=service_policy, devices=devices,
|
|
922
|
+
checkpoint_every=checkpoint_every, max_seconds=max_seconds,
|
|
923
|
+
stop_after_steps=stop_after_steps, preview_every=pe,
|
|
924
|
+
preview_keep=preview_keep, preview_keep_source=preview_keep_source,
|
|
925
|
+
preview_name=preview_name, **extra)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def train(model, run_dir=None, *, steps=None, previews=None, preview_every=None, checkpoint_every=None, profile=None,
|
|
929
|
+
devices=None, on_event=None, **controls):
|
|
930
|
+
"""Train (or continue, if ``run_dir`` exists) and return the ``Run``.
|
|
931
|
+
|
|
932
|
+
If ``run_dir`` is omitted or None, automatically creates `<runs_root>/<slug>`.
|
|
933
|
+
"""
|
|
934
|
+
model_obj = _model(model)
|
|
935
|
+
if run_dir is None:
|
|
936
|
+
run_dir = _default_run_dir(model_obj)
|
|
937
|
+
pe = preview_every if preview_every is not None else previews
|
|
938
|
+
prep = prepare(model_obj, run_dir, steps=steps, profile=profile, devices=devices,
|
|
939
|
+
checkpoint_every=checkpoint_every, preview_every=pe, **controls)
|
|
940
|
+
prep.run(on_event=on_event)
|
|
941
|
+
return open_run(run_dir)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def resume(run, model=None, *, steps=None, devices=None, on_event=None, **controls):
|
|
945
|
+
"""Continue a run from its latest checkpoint (optionally with changed observation settings)."""
|
|
946
|
+
run = _run(run)
|
|
947
|
+
prep = prepare_resume(run.path, model=model, steps=steps, devices=devices, **controls)
|
|
948
|
+
prep.run(on_event=on_event)
|
|
949
|
+
return open_run(run.path)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
# --------------------------------------------------------------------------- reading runs
|
|
953
|
+
|
|
954
|
+
def _events(path):
|
|
955
|
+
from .run_events import read_event_page
|
|
956
|
+
cursor = None
|
|
957
|
+
while True:
|
|
958
|
+
page = read_event_page(path, cursor, limit=1000)
|
|
959
|
+
yield from page['events']
|
|
960
|
+
cursor = page['cursor']
|
|
961
|
+
if not page['has_more']:
|
|
962
|
+
return
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def metrics(run, names=None):
|
|
966
|
+
"""Published scalar metrics: ``{name: [(step, value), ...]}`` in step order."""
|
|
967
|
+
run = _run(run)
|
|
968
|
+
wanted = set(names) if names is not None else None
|
|
969
|
+
series = {}
|
|
970
|
+
for event in _events(run.path):
|
|
971
|
+
for name, value in (event.get('metrics') or {}).items():
|
|
972
|
+
if wanted is None or name in wanted:
|
|
973
|
+
series.setdefault(name, []).append((event['step'], value))
|
|
974
|
+
return series
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def evaluations(run):
|
|
978
|
+
"""Snapshot evaluation results: ``{metric_id: [{step, value, status, ...}, ...]}``."""
|
|
979
|
+
run = _run(run)
|
|
980
|
+
results = {}
|
|
981
|
+
for receipt in sorted((run.path / 'metrics' / 'evaluations').glob('*/receipt.json')):
|
|
982
|
+
data = json.loads(receipt.read_text())
|
|
983
|
+
result = data.get('result') or {}
|
|
984
|
+
results.setdefault(data.get('metric_id'), []).append({
|
|
985
|
+
'step': result.get('step', data.get('source_step')), 'value': result.get('value'),
|
|
986
|
+
'status': data.get('status'), 'evaluation_id': data.get('evaluation_id'),
|
|
987
|
+
'error': result.get('error')})
|
|
988
|
+
for rows in results.values():
|
|
989
|
+
rows.sort(key=lambda row: (row['step'] is None, row['step'] or 0))
|
|
990
|
+
return results
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def observations(run):
|
|
994
|
+
"""Why a value is or is not there: [(step, id, status, reason)] for custom metrics,
|
|
995
|
+
evaluations and previews (queued, dropped, scheduled, skipped, ...)."""
|
|
996
|
+
run = _run(run)
|
|
997
|
+
rows = []
|
|
998
|
+
for event in _events(run.path):
|
|
999
|
+
kind = event.get('event')
|
|
1000
|
+
for name, status in (event.get('measurement_status') or {}).items():
|
|
1001
|
+
if status.get('status') not in ('available', 'unavailable'):
|
|
1002
|
+
rows.append((event['step'], name, status.get('status'), status.get('reason')))
|
|
1003
|
+
if kind in ('evaluation_scheduled', 'evaluation_skipped', 'evaluation_complete'):
|
|
1004
|
+
rows.append((event['step'], event.get('metric_id', 'evaluation'), kind.split('_', 1)[1],
|
|
1005
|
+
event.get('reason')))
|
|
1006
|
+
elif kind in ('preview', 'preview_skipped'):
|
|
1007
|
+
rows.append((event['step'], 'preview', 'published' if kind == 'preview' else 'skipped',
|
|
1008
|
+
event.get('reason')))
|
|
1009
|
+
return rows
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def catalog(run, revision=None):
|
|
1013
|
+
"""Immutable metric definitions recorded by a run."""
|
|
1014
|
+
from .metrics import read_catalog
|
|
1015
|
+
run = _run(run)
|
|
1016
|
+
return read_catalog(run.path, revision)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def events(run, cursor=None, limit=100, max_bytes=1048576):
|
|
1020
|
+
"""Read a bounded page of run events without loading training dependencies."""
|
|
1021
|
+
from .run_events import read_event_page
|
|
1022
|
+
run = _run(run)
|
|
1023
|
+
return read_event_page(run.path, cursor=cursor, limit=limit, max_bytes=max_bytes)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
class InferenceBundle(tuple):
|
|
1027
|
+
"""Loaded inference bundle supporting both 5-tuple unpacking and dictionary access."""
|
|
1028
|
+
|
|
1029
|
+
def __contains__(self, key):
|
|
1030
|
+
if len(self) > 0 and isinstance(self[0], dict) and key in self[0]:
|
|
1031
|
+
return True
|
|
1032
|
+
return super().__contains__(key)
|
|
1033
|
+
|
|
1034
|
+
def __getitem__(self, item):
|
|
1035
|
+
if isinstance(item, str) and len(self) > 0 and isinstance(self[0], dict) and item in self[0]:
|
|
1036
|
+
return self[0][item]
|
|
1037
|
+
return super().__getitem__(item)
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def load_inference(target=None):
|
|
1041
|
+
"""Verify and load a run's EMA inference bundle on CPU in evaluation mode.
|
|
1042
|
+
|
|
1043
|
+
Accepts an exported .pt bundle file, a run directory, slug, tag, short ID, or Run instance.
|
|
1044
|
+
Returns (state_dict, config_dict, ComponentGraph, prior, bundle_sha256).
|
|
1045
|
+
"""
|
|
1046
|
+
import torch
|
|
1047
|
+
from .recipes import ComponentGraph, make_prior
|
|
1048
|
+
from .config import resolve_config
|
|
1049
|
+
|
|
1050
|
+
if isinstance(target, Run):
|
|
1051
|
+
run_dir = target.path
|
|
1052
|
+
is_direct_pt = False
|
|
1053
|
+
elif target is not None and (str(target).endswith(".pt") or (Path(target).is_file() and Path(target).suffix.lower() == ".pt")):
|
|
1054
|
+
is_direct_pt = True
|
|
1055
|
+
pt_path = Path(target).resolve()
|
|
1056
|
+
else:
|
|
1057
|
+
is_direct_pt = False
|
|
1058
|
+
run_dir = _run_path(target)
|
|
1059
|
+
|
|
1060
|
+
if is_direct_pt:
|
|
1061
|
+
path = pt_path
|
|
1062
|
+
checksum_path = pt_path.with_name("model.sha256") if pt_path.name == "model.pt" else pt_path.with_suffix(".sha256")
|
|
1063
|
+
if checksum_path.is_file():
|
|
1064
|
+
expected = checksum_path.read_text(encoding="utf-8").strip()
|
|
1065
|
+
if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
|
|
1066
|
+
raise ValueError("Inference artifact hash mismatch")
|
|
1067
|
+
else:
|
|
1068
|
+
expected = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
1069
|
+
else:
|
|
1070
|
+
bundle_dir = run_dir
|
|
1071
|
+
manifest_path = run_dir / "manifest.json"
|
|
1072
|
+
if manifest_path.is_file():
|
|
1073
|
+
try:
|
|
1074
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
1075
|
+
if manifest.get("bundle_path"):
|
|
1076
|
+
bp = Path(manifest["bundle_path"])
|
|
1077
|
+
bundle_dir = (bp if bp.is_absolute() else run_dir / bp).parent
|
|
1078
|
+
except Exception:
|
|
1079
|
+
pass
|
|
1080
|
+
path = bundle_dir / "model.pt"
|
|
1081
|
+
if not path.is_file():
|
|
1082
|
+
try:
|
|
1083
|
+
from .checkpoints import resolve_checkpoint, export_checkpoint_bundle
|
|
1084
|
+
ckpt_dir = resolve_checkpoint(run_dir, target="latest")
|
|
1085
|
+
path, checksum_path = export_checkpoint_bundle(ckpt_dir, output_dir=run_dir)
|
|
1086
|
+
bundle_dir = run_dir
|
|
1087
|
+
except Exception:
|
|
1088
|
+
pass
|
|
1089
|
+
if not path.is_file():
|
|
1090
|
+
raise FileNotFoundError(f"No inference bundle or checkpoint found for {target or run_dir}")
|
|
1091
|
+
sha_file = bundle_dir / "model.sha256"
|
|
1092
|
+
if sha_file.is_file():
|
|
1093
|
+
expected = sha_file.read_text(encoding="utf-8").strip()
|
|
1094
|
+
if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
|
|
1095
|
+
raise ValueError("Inference artifact hash mismatch")
|
|
1096
|
+
else:
|
|
1097
|
+
expected = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
1098
|
+
|
|
1099
|
+
state = torch.load(path, map_location="cpu", weights_only=True)
|
|
1100
|
+
if not isinstance(state, dict) or state.get("schema_version") != 1 or state.get("kind") != "ema-inference":
|
|
1101
|
+
raise ValueError("Unsupported inference artifact schema")
|
|
1102
|
+
for key in ("config", "components", "model_states", "prior", "example_inputs"):
|
|
1103
|
+
if not isinstance(state.get(key), dict):
|
|
1104
|
+
raise ValueError(f"Invalid inference artifact: missing or invalid {key}")
|
|
1105
|
+
if "generator" not in state["components"] or set(state["model_states"]) != {name for name, spec in state["components"].items() if 'reuse' not in spec}:
|
|
1106
|
+
raise ValueError("Invalid inference artifact: component states must exactly match the generator graph")
|
|
1107
|
+
config = resolve_config(state["config"])
|
|
1108
|
+
graph = ComponentGraph(state["components"]).float().eval().requires_grad_(False)
|
|
1109
|
+
for name, weights in state["model_states"].items():
|
|
1110
|
+
graph.models[name].load_state_dict(weights)
|
|
1111
|
+
if "model_buffers" in state and name in state["model_buffers"]:
|
|
1112
|
+
buf = state["model_buffers"][name]
|
|
1113
|
+
for bname, bval in (buf or {}).items():
|
|
1114
|
+
if hasattr(graph.models[name], bname):
|
|
1115
|
+
getattr(graph.models[name], bname).copy_(bval)
|
|
1116
|
+
prior = make_prior(config["prior"], device='cpu', recipe=config.get('recipe')).float().eval().requires_grad_(False)
|
|
1117
|
+
prior.load_state_dict(state["prior"])
|
|
1118
|
+
if "prior_buffers" in state:
|
|
1119
|
+
for bname, bval in (state["prior_buffers"] or {}).items():
|
|
1120
|
+
if hasattr(prior, bname):
|
|
1121
|
+
getattr(prior, bname).copy_(bval)
|
|
1122
|
+
return InferenceBundle((state, config, graph, prior, expected))
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
@dataclass(frozen=True)
|
|
1126
|
+
class Sample:
|
|
1127
|
+
"""Generated output recorded by a run, in whatever structure the model produces.
|
|
1128
|
+
|
|
1129
|
+
``data`` holds the generated tensor (``None`` when a large image preview was
|
|
1130
|
+
stored only as a PNG grid, then in ``png``). ``inputs`` holds the batch
|
|
1131
|
+
conditioning it was generated from.
|
|
1132
|
+
"""
|
|
1133
|
+
step: int
|
|
1134
|
+
name: str
|
|
1135
|
+
data: object
|
|
1136
|
+
inputs: dict
|
|
1137
|
+
particle_ids: list | None
|
|
1138
|
+
source: str
|
|
1139
|
+
path: Path
|
|
1140
|
+
png: bytes | None = None
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _tensor(value):
|
|
1144
|
+
import torch
|
|
1145
|
+
return None if value is None else torch.tensor(value)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def samples(run, *, name=None):
|
|
1149
|
+
"""Periodic EMA previews, oldest first (enable with ``train(..., previews=N)``)."""
|
|
1150
|
+
import base64
|
|
1151
|
+
run = _run(run)
|
|
1152
|
+
index = run.path / 'previews' / 'index.json'
|
|
1153
|
+
if not index.is_file():
|
|
1154
|
+
return []
|
|
1155
|
+
result = []
|
|
1156
|
+
for entry in json.loads(index.read_text())['previews']:
|
|
1157
|
+
if name is not None and entry.get('name') != name:
|
|
1158
|
+
continue
|
|
1159
|
+
path = Path(entry['path'])
|
|
1160
|
+
payload = json.loads(path.read_text())
|
|
1161
|
+
grid = payload.get('image_grid')
|
|
1162
|
+
result.append(Sample(step=payload['step'], name=payload['name'], data=_tensor(payload.get('samples')),
|
|
1163
|
+
inputs={key: _tensor(value) for key, value in payload.get('inputs', {}).items()
|
|
1164
|
+
if isinstance(value, list)},
|
|
1165
|
+
particle_ids=payload.get('particle_ids'), source='preview', path=path,
|
|
1166
|
+
png=base64.b64decode(grid['png_base64']) if grid else None))
|
|
1167
|
+
return sorted(result, key=lambda item: item.step)
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def sample(run=None, count=16, seed=None, *, inputs=None, output=None, checkpoint=None, default_image_png=False):
|
|
1171
|
+
"""Fresh samples from the run's final EMA model (or target checkpoint or .pt bundle)."""
|
|
1172
|
+
from .artifacts import sample as draw
|
|
1173
|
+
if isinstance(run, (str, Path)) and (str(run).endswith('.pt') or (Path(run).is_file() and Path(run).suffix.lower() == '.pt')):
|
|
1174
|
+
target_path = Path(run)
|
|
1175
|
+
else:
|
|
1176
|
+
run_obj = _run(run)
|
|
1177
|
+
target_path = run_obj.path
|
|
1178
|
+
|
|
1179
|
+
if seed is None:
|
|
1180
|
+
cfg = getattr(run, "config", None) if isinstance(run, Run) else None
|
|
1181
|
+
if cfg is None and target_path.is_dir() and (target_path / "manifest.json").is_file():
|
|
1182
|
+
try:
|
|
1183
|
+
m_data = json.loads((target_path / "manifest.json").read_text(encoding="utf-8"))
|
|
1184
|
+
cfg = m_data.get("config")
|
|
1185
|
+
except Exception:
|
|
1186
|
+
pass
|
|
1187
|
+
seed = (cfg or {}).get("sampling", {}).get("seed", 42) if isinstance(cfg, dict) else 42
|
|
1188
|
+
|
|
1189
|
+
path = Path(draw(target_path, count=count, seed=seed, inputs=inputs, output=output,
|
|
1190
|
+
checkpoint=checkpoint, default_image_png=default_image_png))
|
|
1191
|
+
if path.suffix.lower() == '.png':
|
|
1192
|
+
from PIL import Image
|
|
1193
|
+
with Image.open(path, formats=["PNG"]) as img:
|
|
1194
|
+
raw_meta = img.info.get('hypergan', '{}')
|
|
1195
|
+
payload = json.loads(raw_meta) if raw_meta else {}
|
|
1196
|
+
return Sample(step=payload.get('step', 0), name='sample', data=None,
|
|
1197
|
+
inputs={}, particle_ids=payload.get('particle_ids'),
|
|
1198
|
+
source='inference', path=path, png=path.read_bytes())
|
|
1199
|
+
payload = json.loads(path.read_text())
|
|
1200
|
+
return Sample(step=payload['step'], name='sample', data=_tensor(payload['samples']),
|
|
1201
|
+
inputs={key: _tensor(value) for key, value in payload.get('inputs', {}).items()},
|
|
1202
|
+
particle_ids=payload.get('particle_ids'), source='inference', path=path)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def view(item, sampler, *, run=None):
|
|
1206
|
+
"""Apply a sampler to a Sample: a name from the model's ``[samplers]``, a
|
|
1207
|
+
``module:function`` path, or a callable. Returns what the sampler returns
|
|
1208
|
+
(``Image``/``Audio`` for the built-in helpers)."""
|
|
1209
|
+
from .item_data import import_object
|
|
1210
|
+
args = {}
|
|
1211
|
+
if isinstance(sampler, str) and ':' not in sampler:
|
|
1212
|
+
config = _run(run).config if run is not None else None
|
|
1213
|
+
declared = (config or {}).get('samplers', {})
|
|
1214
|
+
if sampler not in declared:
|
|
1215
|
+
raise KeyError(f'Sampler {sampler!r} is not declared in this run ({", ".join(declared) or "none"})')
|
|
1216
|
+
args = declared[sampler]['args']
|
|
1217
|
+
sampler = declared[sampler]['fn']
|
|
1218
|
+
fn = import_object(sampler) if isinstance(sampler, str) else sampler
|
|
1219
|
+
return fn(item, **args)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def evaluate(run, metric_id=None, *, checkpoint=None, devices=None, model=None, config=None, bundle=None):
|
|
1223
|
+
"""Run one snapshot evaluation now on a run (finished or running); returns its receipt."""
|
|
1224
|
+
from .metric_evaluation import evaluate as run_evaluation
|
|
1225
|
+
run = _run(run)
|
|
1226
|
+
config_p = lower(model) if model is not None else config
|
|
1227
|
+
return run_evaluation(run.path, metric_id, checkpoint=checkpoint, devices=devices,
|
|
1228
|
+
config_path=config_p, bundle=bundle)
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def resolve_run_target(target=None, *, cwd=None, allow_latest=True, require_active=False):
|
|
1232
|
+
"""Resolve a run reference (short ID, slug, name, path, @latest, or None for active/latest)."""
|
|
1233
|
+
from .registry import resolve_run_target as _resolve_target
|
|
1234
|
+
return _resolve_target(target, cwd=cwd, allow_latest=allow_latest, require_active=require_active)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def register_run(run_dir, **kwargs):
|
|
1238
|
+
"""Register a run directory in the global runs registry (runs.json)."""
|
|
1239
|
+
from .registry import register_run as _register
|
|
1240
|
+
return _register(run_dir, **kwargs)
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def sync_registry(cwd=None, extra_roots=None):
|
|
1244
|
+
"""Synchronize runs in workspace and runs roots into the global registry."""
|
|
1245
|
+
from .registry import sync_registry as _sync
|
|
1246
|
+
return _sync(cwd=cwd, extra_roots=extra_roots)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def generate_run_slug(stem, parent_dir=None):
|
|
1250
|
+
"""Generate a unique run name and short '<adj>-<noun>' slug for a model stem."""
|
|
1251
|
+
from .registry import generate_run_slug as _gen_slug
|
|
1252
|
+
return _gen_slug(stem, parent_dir=parent_dir)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def export(run=None, checkpoint="latest", output=None):
|
|
1256
|
+
"""Export a standalone EMA inference model bundle from a run checkpoint."""
|
|
1257
|
+
from .checkpoints import resolve_checkpoint as _resolve, export_checkpoint_bundle as _export
|
|
1258
|
+
run_dir = _run_path(run)
|
|
1259
|
+
ckpt_dir = _resolve(run_dir, target=checkpoint)
|
|
1260
|
+
dest, _ = _export(ckpt_dir, output_dir=output or run_dir)
|
|
1261
|
+
return dest
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def prune_checkpoints(run=None, policy=None, dry_run=False):
|
|
1265
|
+
"""Prune obsolete checkpoints in run under the retention policy."""
|
|
1266
|
+
from .checkpoints import prune_checkpoints as _prune
|
|
1267
|
+
return _prune(_run_path(run), policy=policy, dry_run=dry_run)
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def resolve_checkpoint(run=None, target="latest"):
|
|
1271
|
+
"""Resolve target ('latest', 'best', step int/str, or path) to a checkpoint directory."""
|
|
1272
|
+
from .checkpoints import resolve_checkpoint as _resolve
|
|
1273
|
+
return _resolve(_run_path(run), target=target)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def extract_ema_bundle(checkpoint_dir, manifest=None):
|
|
1277
|
+
"""Extract EMA weights from checkpoint_dir into an in-memory bundle."""
|
|
1278
|
+
from .checkpoints import extract_ema_bundle as _extract
|
|
1279
|
+
return _extract(checkpoint_dir, manifest=manifest)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def load_bundle(path_or_run, *, checkpoint="latest", device="cpu"):
|
|
1283
|
+
"""Load an exported .pt bundle file or extract the EMA bundle from a run checkpoint."""
|
|
1284
|
+
if isinstance(path_or_run, Run):
|
|
1285
|
+
run_dir = path_or_run.path
|
|
1286
|
+
else:
|
|
1287
|
+
p = Path(path_or_run)
|
|
1288
|
+
if p.suffix.lower() == ".pt" or (p.is_file() and p.name.endswith(".pt")):
|
|
1289
|
+
import torch
|
|
1290
|
+
return torch.load(p, map_location=device, weights_only=True)
|
|
1291
|
+
run_dir = _run_path(path_or_run)
|
|
1292
|
+
try:
|
|
1293
|
+
ckpt_dir = resolve_checkpoint(run_dir, target=checkpoint)
|
|
1294
|
+
return extract_ema_bundle(ckpt_dir)
|
|
1295
|
+
except FileNotFoundError:
|
|
1296
|
+
model_pt = run_dir / "model.pt"
|
|
1297
|
+
if model_pt.is_file():
|
|
1298
|
+
import torch
|
|
1299
|
+
return torch.load(model_pt, map_location=device, weights_only=True)
|
|
1300
|
+
raise
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def inspect(run=None):
|
|
1304
|
+
"""Inspect a run and return its parsed manifest and run identity dictionary."""
|
|
1305
|
+
if isinstance(run, Run):
|
|
1306
|
+
run_path = run.path
|
|
1307
|
+
data = dict(run.manifest)
|
|
1308
|
+
else:
|
|
1309
|
+
try:
|
|
1310
|
+
opened = open_run(run)
|
|
1311
|
+
run_path = opened.path
|
|
1312
|
+
data = dict(opened.manifest)
|
|
1313
|
+
except FileNotFoundError:
|
|
1314
|
+
run_path = _run_path(run)
|
|
1315
|
+
if run_path.is_dir():
|
|
1316
|
+
return status(run_path)
|
|
1317
|
+
raise
|
|
1318
|
+
data.setdefault("run_dir", str(run_path))
|
|
1319
|
+
summary_p = run_path / "summary.json"
|
|
1320
|
+
summary = {}
|
|
1321
|
+
if summary_p.is_file():
|
|
1322
|
+
try:
|
|
1323
|
+
loaded = json.loads(summary_p.read_text(encoding="utf-8"))
|
|
1324
|
+
if isinstance(loaded, dict):
|
|
1325
|
+
summary = loaded
|
|
1326
|
+
except Exception:
|
|
1327
|
+
summary = {}
|
|
1328
|
+
if "name" not in data:
|
|
1329
|
+
cfg_name = data.get("config", {}).get("name") if isinstance(data.get("config"), dict) else None
|
|
1330
|
+
data["name"] = summary.get("name") or cfg_name or run_path.name
|
|
1331
|
+
elif summary.get("name") and data.get("name") != summary.get("name"):
|
|
1332
|
+
data["name"] = summary["name"]
|
|
1333
|
+
if "slug" not in data:
|
|
1334
|
+
from .registry import extract_or_derive_slug
|
|
1335
|
+
run_id = str(data.get("run_id") or summary.get("run_id") or run_path.name)
|
|
1336
|
+
data["slug"] = summary.get("slug") or extract_or_derive_slug(run_id, name=str(data.get("name", "")), run_dir_name=run_path.name)
|
|
1337
|
+
if "short_id" not in data and (data.get("run_id") or summary.get("run_id")):
|
|
1338
|
+
data["short_id"] = str(data.get("run_id") or summary.get("run_id"))[:8]
|
|
1339
|
+
if "step" not in data:
|
|
1340
|
+
if "step" in summary:
|
|
1341
|
+
data["step"] = summary["step"]
|
|
1342
|
+
elif "steps" in data:
|
|
1343
|
+
data["step"] = data["steps"]
|
|
1344
|
+
return data
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def status(run=None):
|
|
1348
|
+
"""Compile human-friendly status summary for a run."""
|
|
1349
|
+
from .inspection import run_status as _status
|
|
1350
|
+
return _status(_run_path(run))
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def ps(all_runs=False, cwd=None, *, all=None, root=None):
|
|
1354
|
+
"""List active training runs (or all runs if all_runs=True) enriched with GPU VRAM and queue status."""
|
|
1355
|
+
if all is not None:
|
|
1356
|
+
all_runs = bool(all)
|
|
1357
|
+
target_cwd = root if root is not None else cwd
|
|
1358
|
+
from .inspection import ps_runs as _ps
|
|
1359
|
+
return _ps(all_runs=all_runs, cwd=target_cwd)
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def stop(run=None, *, stop_all=False, timeout=15.0):
|
|
1363
|
+
"""Gracefully stop an active training run (or all active runs) via SIGINT."""
|
|
1364
|
+
from .inspection import stop_run as _stop
|
|
1365
|
+
return _stop(run, stop_all=stop_all, timeout=timeout)
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def logs(run=None, *, tail=50, follow=False, raw=False, stream=None):
|
|
1369
|
+
"""Read or follow formatted training events from a run."""
|
|
1370
|
+
import io
|
|
1371
|
+
from .inspection import read_run_logs as _logs
|
|
1372
|
+
if stream is None and not follow:
|
|
1373
|
+
frame = _py_inspect.currentframe()
|
|
1374
|
+
caller = frame.f_back if frame is not None else None
|
|
1375
|
+
caller_mod = caller.f_globals.get("__name__", "") if caller is not None else ""
|
|
1376
|
+
target_stream = None if caller_mod.endswith(".cli") or caller_mod == "hypergan.cli" else io.StringIO()
|
|
1377
|
+
else:
|
|
1378
|
+
target_stream = stream
|
|
1379
|
+
if raw:
|
|
1380
|
+
sig = _py_inspect.signature(_logs)
|
|
1381
|
+
if "raw" in sig.parameters:
|
|
1382
|
+
return _logs(run, tail=tail, follow=follow, raw=raw, stream=target_stream)
|
|
1383
|
+
run_dir = _run_path(run)
|
|
1384
|
+
events_file = run_dir / "events.jsonl"
|
|
1385
|
+
if not events_file.is_file():
|
|
1386
|
+
return []
|
|
1387
|
+
raw_lines = [ln for ln in events_file.read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()]
|
|
1388
|
+
if tail and tail > 0:
|
|
1389
|
+
raw_lines = raw_lines[-tail:]
|
|
1390
|
+
out = target_stream or sys.stdout
|
|
1391
|
+
for ln in raw_lines:
|
|
1392
|
+
out.write(ln + "\n")
|
|
1393
|
+
if hasattr(out, "flush"):
|
|
1394
|
+
out.flush()
|
|
1395
|
+
return raw_lines
|
|
1396
|
+
return _logs(run, tail=tail, follow=follow, stream=target_stream)
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def tag(run, name):
|
|
1400
|
+
"""Rename or tag a training run in its manifest, summary, and global registry."""
|
|
1401
|
+
from . import inspection
|
|
1402
|
+
if hasattr(inspection, "tag_run"):
|
|
1403
|
+
res = dict(inspection.tag_run(run, name))
|
|
1404
|
+
run_id = str(res.get("run_id") or res.get("short_id") or "")
|
|
1405
|
+
r_dir = res.get("run_dir", "")
|
|
1406
|
+
r_name = res.get("name", name)
|
|
1407
|
+
res.setdefault("short_id", run_id[:8])
|
|
1408
|
+
res.setdefault("message", f"Tagged {run_id[:8]} as '{r_name}' ({r_dir})")
|
|
1409
|
+
return res
|
|
1410
|
+
|
|
1411
|
+
from .summary import atomic_write_json, read_summary_json
|
|
1412
|
+
run_dir = _run_path(run)
|
|
1413
|
+
if not run_dir.is_dir():
|
|
1414
|
+
raise FileNotFoundError(f"Run directory not found: {run_dir}")
|
|
1415
|
+
new_name = str(name).strip()
|
|
1416
|
+
if not new_name:
|
|
1417
|
+
raise ValueError("Tag name must be a non-empty string")
|
|
1418
|
+
manifest_p = run_dir / "manifest.json"
|
|
1419
|
+
manifest = {}
|
|
1420
|
+
if manifest_p.is_file():
|
|
1421
|
+
try:
|
|
1422
|
+
manifest = json.loads(manifest_p.read_text(encoding="utf-8"))
|
|
1423
|
+
except Exception:
|
|
1424
|
+
manifest = {}
|
|
1425
|
+
manifest["name"] = new_name
|
|
1426
|
+
if isinstance(manifest.get("config"), dict):
|
|
1427
|
+
manifest["config"]["name"] = new_name
|
|
1428
|
+
atomic_write_json(manifest_p, manifest)
|
|
1429
|
+
summary_p = run_dir / "summary.json"
|
|
1430
|
+
summary = read_summary_json(run_dir) or {}
|
|
1431
|
+
if summary_p.is_file() or summary:
|
|
1432
|
+
summary["name"] = new_name
|
|
1433
|
+
atomic_write_json(summary_p, summary)
|
|
1434
|
+
run_id = str(summary.get("run_id") or manifest.get("run_id") or run_dir.name)
|
|
1435
|
+
register_run(run_dir, name=new_name)
|
|
1436
|
+
return {
|
|
1437
|
+
"run_id": run_id,
|
|
1438
|
+
"short_id": run_id[:8],
|
|
1439
|
+
"name": new_name,
|
|
1440
|
+
"run_dir": str(run_dir),
|
|
1441
|
+
"message": f"Tagged {run_id[:8]} as '{new_name}' ({run_dir})",
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def rm(run, *, force=False):
|
|
1446
|
+
"""Remove a run directory and its registry entry (stops active run if force=True)."""
|
|
1447
|
+
from . import inspection
|
|
1448
|
+
if hasattr(inspection, "remove_run"):
|
|
1449
|
+
res = dict(inspection.remove_run(run, force=force))
|
|
1450
|
+
run_id = str(res.get("run_id") or res.get("short_id") or "")
|
|
1451
|
+
r_dir = res.get("run_dir", "")
|
|
1452
|
+
r_name = res.get("name", Path(r_dir).name if r_dir else "")
|
|
1453
|
+
res.setdefault("short_id", run_id[:8])
|
|
1454
|
+
res.setdefault("message", f"Removed run '{r_name}' ({run_id[:8]}) at {r_dir}")
|
|
1455
|
+
return res
|
|
1456
|
+
|
|
1457
|
+
import shutil
|
|
1458
|
+
from .summary import read_summary_json
|
|
1459
|
+
run_dir = _run_path(run)
|
|
1460
|
+
if not run_dir.exists():
|
|
1461
|
+
raise FileNotFoundError(f"Run directory not found: {run_dir}")
|
|
1462
|
+
is_active, _ = inspection.check_run_active(run_dir)
|
|
1463
|
+
if is_active:
|
|
1464
|
+
if not force:
|
|
1465
|
+
raise RuntimeError(f"Cannot remove active run '{run_dir.name}' without --force")
|
|
1466
|
+
try:
|
|
1467
|
+
stop(run_dir, timeout=10.0)
|
|
1468
|
+
except Exception:
|
|
1469
|
+
pass
|
|
1470
|
+
summary = read_summary_json(run_dir) or {}
|
|
1471
|
+
manifest = {}
|
|
1472
|
+
if (run_dir / "manifest.json").is_file():
|
|
1473
|
+
try:
|
|
1474
|
+
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
|
|
1475
|
+
except Exception:
|
|
1476
|
+
pass
|
|
1477
|
+
run_id = str(summary.get("run_id") or manifest.get("run_id") or run_dir.name)
|
|
1478
|
+
r_name = str(summary.get("name") or manifest.get("name") or manifest.get("config", {}).get("name") or run_dir.name)
|
|
1479
|
+
shutil.rmtree(run_dir)
|
|
1480
|
+
sync_registry(cwd=run_dir.parent)
|
|
1481
|
+
return {
|
|
1482
|
+
"run_id": run_id,
|
|
1483
|
+
"short_id": run_id[:8],
|
|
1484
|
+
"name": r_name,
|
|
1485
|
+
"run_dir": str(run_dir),
|
|
1486
|
+
"removed": True,
|
|
1487
|
+
"message": f"Removed run '{r_name}' ({run_id[:8]}) at {run_dir}",
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def list_runs(root=None, *, sort="updated", status=None, status_filter=None, cwd=None):
|
|
1492
|
+
"""Discover and list all runs under root with filtering and sorting."""
|
|
1493
|
+
from .inspection import list_runs as _list_runs
|
|
1494
|
+
filt = status if status is not None else status_filter
|
|
1495
|
+
return _list_runs(root, sort=sort, status_filter=filt, cwd=cwd)
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
runs = list_runs
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def runs_ls(root=None, sort="fid", status_filter=None, *, status=None, cwd=None):
|
|
1502
|
+
"""Discover and list all runs under root with filtering and sorting."""
|
|
1503
|
+
return list_runs(root, sort=sort, status=status, status_filter=status_filter, cwd=cwd)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def runs_compare(runs, metric="fid50k_train", at_steps=None):
|
|
1507
|
+
"""Compare multiple runs for a given metric across step milestones."""
|
|
1508
|
+
from .inspection import compare_runs as _compare_runs
|
|
1509
|
+
return _compare_runs(runs, metric=metric, at_steps=at_steps)
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def gc(run=None, dry_run=False, stride=10, *, all_runs=False, all=None):
|
|
1513
|
+
"""Clean rebuildable views and compact events on a finished run (or all runs if all_runs=True)."""
|
|
1514
|
+
if all is not None:
|
|
1515
|
+
all_runs = bool(all)
|
|
1516
|
+
if all_runs:
|
|
1517
|
+
return gc_all(root=run, dry_run=dry_run, stride=stride)
|
|
1518
|
+
from .inspection import gc_run as _gc_run
|
|
1519
|
+
return _gc_run(_run_path(run), dry_run=dry_run, stride=stride)
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
prune = gc
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
def gc_all(root=None, dry_run=False, stride=10):
|
|
1526
|
+
"""Clean all finished runs under root."""
|
|
1527
|
+
from .inspection import gc_all as _gc_all
|
|
1528
|
+
return _gc_all(root, dry_run=dry_run, stride=stride)
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def compact_events(run, stride=10):
|
|
1532
|
+
"""Compact redundant train events in events.jsonl, returning reclaimed bytes."""
|
|
1533
|
+
from .inspection import compact_events as _compact
|
|
1534
|
+
return _compact(_run_path(run), stride=stride)
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
# --------------------------------------------------------------------------- viewables
|
|
1538
|
+
|
|
1539
|
+
@dataclass(frozen=True)
|
|
1540
|
+
class Image:
|
|
1541
|
+
"""8-bit pixels, row-major HWC (1 or 3 channels)."""
|
|
1542
|
+
pixels: bytes
|
|
1543
|
+
width: int
|
|
1544
|
+
height: int
|
|
1545
|
+
channels: int
|
|
1546
|
+
|
|
1547
|
+
def png(self):
|
|
1548
|
+
from .image_grids import encode_png
|
|
1549
|
+
return encode_png(self.pixels, self.width, self.height, self.channels)
|
|
1550
|
+
|
|
1551
|
+
def save(self, path):
|
|
1552
|
+
Path(path).write_bytes(self.png())
|
|
1553
|
+
return Path(path)
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
@dataclass(frozen=True)
|
|
1557
|
+
class Audio:
|
|
1558
|
+
"""Mono or interleaved float samples in [-1, 1]."""
|
|
1559
|
+
samples: list
|
|
1560
|
+
rate: int
|
|
1561
|
+
channels: int = 1
|
|
1562
|
+
|
|
1563
|
+
def save(self, path):
|
|
1564
|
+
import struct
|
|
1565
|
+
import wave
|
|
1566
|
+
with wave.open(str(path), 'wb') as stream:
|
|
1567
|
+
stream.setnchannels(self.channels)
|
|
1568
|
+
stream.setsampwidth(2)
|
|
1569
|
+
stream.setframerate(self.rate)
|
|
1570
|
+
stream.writeframes(b''.join(struct.pack('<h', int(max(-1.0, min(1.0, x)) * 32767)) for x in self.samples))
|
|
1571
|
+
return Path(path)
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def image(tensor, *, low=-1.0, high=1.0):
|
|
1575
|
+
"""An ``Image`` from a CHW or HW tensor/array in [low, high]."""
|
|
1576
|
+
import torch
|
|
1577
|
+
value = torch.as_tensor(tensor, dtype=torch.float32).detach().cpu()
|
|
1578
|
+
if value.ndim == 2:
|
|
1579
|
+
value = value[None]
|
|
1580
|
+
if value.ndim != 3 or value.shape[0] not in (1, 3):
|
|
1581
|
+
raise ValueError('image() expects CHW with 1 or 3 channels, or HW')
|
|
1582
|
+
pixels = ((value - low) / (high - low)).clamp(0, 1).mul(255).round().to(torch.uint8)
|
|
1583
|
+
channels, height, width = pixels.shape
|
|
1584
|
+
return Image(pixels.permute(1, 2, 0).contiguous().numpy().tobytes(), width, height, channels)
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def audio(tensor, *, rate):
|
|
1588
|
+
"""Construct an Audio object from a 1D (mono) or 2D (channels, samples) tensor/array with sample rate."""
|
|
1589
|
+
import torch
|
|
1590
|
+
value = torch.as_tensor(tensor, dtype=torch.float32).detach().cpu()
|
|
1591
|
+
channels = 1 if value.ndim == 1 else value.shape[0]
|
|
1592
|
+
return Audio(value.T.reshape(-1).tolist() if value.ndim == 2 else value.tolist(), rate, channels)
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
# --------------------------------------------------------------------------- recipe lifting & CLI services
|
|
1596
|
+
|
|
1597
|
+
def load_recipe(path):
|
|
1598
|
+
"""Read a recipe TOML file without lowering or resolving."""
|
|
1599
|
+
path = Path(path)
|
|
1600
|
+
with path.open('rb') as stream:
|
|
1601
|
+
return tomllib.load(stream)
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
def lift(raw, *, base=None):
|
|
1605
|
+
"""Lift an engine recipe dictionary into a Model conforming to hypergan-model/1."""
|
|
1606
|
+
from .config import DEFAULT
|
|
1607
|
+
from .model_file import PENALTY_FIELDS
|
|
1608
|
+
if isinstance(raw, Model):
|
|
1609
|
+
return raw if is_model_file(raw.spec) else lift(raw.spec, base=base or raw.base)
|
|
1610
|
+
raw = deepcopy(raw)
|
|
1611
|
+
base = Path.cwd() if base is None else Path(base)
|
|
1612
|
+
if is_model_file(raw):
|
|
1613
|
+
return from_dict(raw, base=base)
|
|
1614
|
+
components = raw.get('components', {}) or deepcopy(DEFAULT.get('components', {}))
|
|
1615
|
+
networks = {}
|
|
1616
|
+
losses = []
|
|
1617
|
+
|
|
1618
|
+
critics_in_terms = {term.get('component') for term in raw.get('adversarial_terms', [])}
|
|
1619
|
+
if 'discriminator' in components:
|
|
1620
|
+
critics_in_terms.add('discriminator')
|
|
1621
|
+
|
|
1622
|
+
for name, spec in components.items():
|
|
1623
|
+
if name == 'discriminator' or name in critics_in_terms:
|
|
1624
|
+
role = 'critic'
|
|
1625
|
+
elif name == 'generator':
|
|
1626
|
+
role = 'generator'
|
|
1627
|
+
elif 'encoder' in name or spec.get('factory', '').endswith('Encoder'):
|
|
1628
|
+
role = 'encoder'
|
|
1629
|
+
else:
|
|
1630
|
+
role = 'auxiliary'
|
|
1631
|
+
|
|
1632
|
+
net = {'role': role, 'inputs': dict(spec.get('inputs', {}))}
|
|
1633
|
+
if 'trainable' in spec:
|
|
1634
|
+
net['trainable'] = spec['trainable']
|
|
1635
|
+
if 'freeze_parameters' in spec:
|
|
1636
|
+
net['freeze_parameters'] = spec['freeze_parameters']
|
|
1637
|
+
if 'reuse' in spec:
|
|
1638
|
+
net['reuse'] = spec['reuse']
|
|
1639
|
+
elif spec.get('factory') == 'hndl':
|
|
1640
|
+
args = dict(spec.get('args', {}))
|
|
1641
|
+
if 'file' in args:
|
|
1642
|
+
net['hndl'] = args.pop('file')
|
|
1643
|
+
elif 'source' in args:
|
|
1644
|
+
net['source'] = args.pop('source')
|
|
1645
|
+
if 'input_shape' in args:
|
|
1646
|
+
net['input_shape'] = args.pop('input_shape')
|
|
1647
|
+
if 'output_shape' in args:
|
|
1648
|
+
net['output_shape'] = args.pop('output_shape')
|
|
1649
|
+
if args:
|
|
1650
|
+
net['args'] = args
|
|
1651
|
+
else:
|
|
1652
|
+
net['module'] = spec.get('factory')
|
|
1653
|
+
if spec.get('args'):
|
|
1654
|
+
net['args'] = dict(spec['args'])
|
|
1655
|
+
networks[name] = net
|
|
1656
|
+
|
|
1657
|
+
adv_idx = None
|
|
1658
|
+
if 'adversarial' in raw:
|
|
1659
|
+
adv_idx = len(losses)
|
|
1660
|
+
losses.append({'type': 'adversarial', 'critic': 'discriminator', 'weight': raw['adversarial'].get('weight', 1.0)})
|
|
1661
|
+
elif 'discriminator' in components:
|
|
1662
|
+
adv_idx = len(losses)
|
|
1663
|
+
losses.append({'type': 'adversarial', 'critic': 'discriminator', 'weight': 1.0})
|
|
1664
|
+
|
|
1665
|
+
if adv_idx is not None and 'gradient_penalty' in raw and 'coeff' in raw['gradient_penalty']:
|
|
1666
|
+
losses[adv_idx]['penalty'] = float(raw['gradient_penalty']['coeff'])
|
|
1667
|
+
|
|
1668
|
+
for term in raw.get('adversarial_terms', []):
|
|
1669
|
+
entry = {'type': 'adversarial', 'critic': term['component'], 'weight': term.get('weight', 1.0)}
|
|
1670
|
+
for k in ('real', 'fake', 'inputs', 'id'):
|
|
1671
|
+
if k in term:
|
|
1672
|
+
entry[k] = term[k]
|
|
1673
|
+
if term.get('penalty') is False:
|
|
1674
|
+
entry['penalty'] = 0.0
|
|
1675
|
+
elif term.get('penalty_coeff') is not None:
|
|
1676
|
+
entry['penalty'] = float(term['penalty_coeff'])
|
|
1677
|
+
losses.append(entry)
|
|
1678
|
+
|
|
1679
|
+
for term in raw.get('objectives', []):
|
|
1680
|
+
factory = term.get('factory', 'mse')
|
|
1681
|
+
args = dict(term.get('args', {}))
|
|
1682
|
+
if factory == 'hypergan.plugin_functions:FunctionLoss' and 'fn' in args:
|
|
1683
|
+
fn = args.pop('fn')
|
|
1684
|
+
else:
|
|
1685
|
+
fn = factory
|
|
1686
|
+
kind = 'reconstruction' if term.get('id') == 'reconstruction' else 'objective'
|
|
1687
|
+
entry = {'type': kind, 'id': term.get('id', 'loss'), 'fn': fn, 'weight': term.get('weight', 1.0)}
|
|
1688
|
+
if 'inputs' in term:
|
|
1689
|
+
entry['inputs'] = dict(term['inputs'])
|
|
1690
|
+
if 'detach' in term:
|
|
1691
|
+
entry['detach'] = list(term['detach'])
|
|
1692
|
+
if args:
|
|
1693
|
+
entry['args'] = args
|
|
1694
|
+
losses.append(entry)
|
|
1695
|
+
|
|
1696
|
+
if 'prior_regularizer' in raw:
|
|
1697
|
+
losses.append({'type': 'prior', **deepcopy(raw['prior_regularizer'])})
|
|
1698
|
+
|
|
1699
|
+
model_spec = {'format': FORMAT, 'networks': networks, 'losses': losses}
|
|
1700
|
+
if 'name' in raw:
|
|
1701
|
+
model_spec['name'] = raw['name']
|
|
1702
|
+
if 'defaults' in raw:
|
|
1703
|
+
model_spec['defaults'] = raw['defaults']
|
|
1704
|
+
if 'run' in raw:
|
|
1705
|
+
model_spec['run'] = deepcopy(raw['run'])
|
|
1706
|
+
if 'checkpoints' in raw:
|
|
1707
|
+
model_spec['checkpoints'] = deepcopy(raw['checkpoints'])
|
|
1708
|
+
if 'data' in raw:
|
|
1709
|
+
data = deepcopy(raw['data'])
|
|
1710
|
+
if data.get('factory') == 'hypergan.item_data:ItemData' and 'dataset' in data.get('args', {}):
|
|
1711
|
+
model_spec['data'] = {'dataset': data['args']['dataset'], **{k: v for k, v in data['args'].items() if k != 'dataset'}}
|
|
1712
|
+
else:
|
|
1713
|
+
model_spec['data'] = data
|
|
1714
|
+
if 'prior' in raw:
|
|
1715
|
+
prior_spec = deepcopy(raw['prior'])
|
|
1716
|
+
prior_args = prior_spec.pop('args', None)
|
|
1717
|
+
if isinstance(prior_args, dict):
|
|
1718
|
+
for k, v in prior_args.items():
|
|
1719
|
+
prior_spec.setdefault(k, v)
|
|
1720
|
+
model_spec['prior'] = prior_spec
|
|
1721
|
+
if 'training' in raw or 'optimizer' in raw:
|
|
1722
|
+
train_table = deepcopy(raw.get('training', {}))
|
|
1723
|
+
if 'optimizer' in raw:
|
|
1724
|
+
train_table.update(deepcopy(raw['optimizer']))
|
|
1725
|
+
model_spec['train'] = train_table
|
|
1726
|
+
gp = raw.get('penalty') or raw.get('gradient_penalty')
|
|
1727
|
+
if gp:
|
|
1728
|
+
penalty_table = {k: deepcopy(v) for k, v in gp.items() if k != 'coeff' and k in PENALTY_FIELDS}
|
|
1729
|
+
if penalty_table:
|
|
1730
|
+
model_spec['penalty'] = penalty_table
|
|
1731
|
+
if 'sampling' in raw:
|
|
1732
|
+
sampling_spec = deepcopy(raw['sampling'])
|
|
1733
|
+
sampling_spec.pop('output_noise', None)
|
|
1734
|
+
if 'generated' in sampling_spec:
|
|
1735
|
+
sampling_spec['output'] = sampling_spec.pop('generated')
|
|
1736
|
+
model_spec['sampling'] = sampling_spec
|
|
1737
|
+
if 'samplers' in raw:
|
|
1738
|
+
model_spec['samplers'] = deepcopy(raw['samplers'])
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
if 'metrics' in raw:
|
|
1742
|
+
metrics_table = deepcopy(raw['metrics'])
|
|
1743
|
+
custom = metrics_table.pop('custom', {})
|
|
1744
|
+
scalar_custom = {}
|
|
1745
|
+
evaluations = {}
|
|
1746
|
+
for name, metric in custom.items():
|
|
1747
|
+
if 'evaluation' in metric or metric.get('mode') == 'snapshot' or metric.get('trigger') in ('interval', 'manual'):
|
|
1748
|
+
eval_entry = {}
|
|
1749
|
+
if 'factory' in metric:
|
|
1750
|
+
eval_entry['factory'] = metric['factory']
|
|
1751
|
+
if 'fn' in metric:
|
|
1752
|
+
eval_entry['fn'] = metric['fn']
|
|
1753
|
+
if 'args' in metric:
|
|
1754
|
+
eval_entry['args'] = deepcopy(metric['args'])
|
|
1755
|
+
if 'inputs' in metric:
|
|
1756
|
+
norm_inputs = {}
|
|
1757
|
+
for ik, iv in metric['inputs'].items():
|
|
1758
|
+
if iv == 'evaluation.generated':
|
|
1759
|
+
norm_inputs[ik] = 'generated'
|
|
1760
|
+
elif iv == 'evaluation.reference':
|
|
1761
|
+
norm_inputs[ik] = 'reference'
|
|
1762
|
+
else:
|
|
1763
|
+
norm_inputs[ik] = iv
|
|
1764
|
+
eval_entry['inputs'] = norm_inputs
|
|
1765
|
+
if metric.get('trigger') == 'interval' and 'every_steps' in metric:
|
|
1766
|
+
eval_entry['every'] = metric['every_steps']
|
|
1767
|
+
elif 'every_steps' in metric and metric.get('trigger') != 'manual':
|
|
1768
|
+
eval_entry['every'] = metric['every_steps']
|
|
1769
|
+
for k in ('timeout', 'on_error', 'on_busy'):
|
|
1770
|
+
if k in metric:
|
|
1771
|
+
eval_entry[k] = metric[k]
|
|
1772
|
+
if 'factory' not in eval_entry and eval_entry.get('kind') != 'factory':
|
|
1773
|
+
for k in ('label', 'unit', 'direction', 'description', 'reduce', 'kind'):
|
|
1774
|
+
if k in metric:
|
|
1775
|
+
eval_entry[k] = metric[k]
|
|
1776
|
+
proto = metric.get('evaluation', {})
|
|
1777
|
+
if isinstance(proto, str):
|
|
1778
|
+
if 'factory' not in eval_entry and eval_entry.get('kind') != 'factory':
|
|
1779
|
+
eval_entry.setdefault('kind', proto)
|
|
1780
|
+
proto = {}
|
|
1781
|
+
elif not isinstance(proto, dict):
|
|
1782
|
+
proto = {}
|
|
1783
|
+
if 'device' in proto:
|
|
1784
|
+
eval_entry['device'] = proto['device']
|
|
1785
|
+
elif 'device' in metric:
|
|
1786
|
+
eval_entry['device'] = metric['device']
|
|
1787
|
+
if 'sample_count' in proto:
|
|
1788
|
+
eval_entry['samples'] = proto['sample_count']
|
|
1789
|
+
elif 'samples' in proto:
|
|
1790
|
+
eval_entry['samples'] = proto['samples']
|
|
1791
|
+
elif 'sample_count' in metric:
|
|
1792
|
+
eval_entry['samples'] = metric['sample_count']
|
|
1793
|
+
elif 'samples' in metric:
|
|
1794
|
+
eval_entry['samples'] = metric['samples']
|
|
1795
|
+
if 'batch_size' in proto:
|
|
1796
|
+
eval_entry['batch_size'] = proto['batch_size']
|
|
1797
|
+
elif 'batch_size' in metric:
|
|
1798
|
+
eval_entry['batch_size'] = metric['batch_size']
|
|
1799
|
+
if 'seed' in proto:
|
|
1800
|
+
eval_entry['seed'] = proto['seed']
|
|
1801
|
+
elif 'seed' in metric:
|
|
1802
|
+
eval_entry['seed'] = metric['seed']
|
|
1803
|
+
proto_data = proto.get('data') or metric.get('data')
|
|
1804
|
+
if proto_data is not None:
|
|
1805
|
+
d = deepcopy(proto_data)
|
|
1806
|
+
if d.get('factory') == 'hypergan.item_data:ItemData' and 'dataset' in d.get('args', {}):
|
|
1807
|
+
eval_entry['data'] = {'dataset': d['args']['dataset'], **{k: v for k, v in d['args'].items() if k != 'dataset'}}
|
|
1808
|
+
else:
|
|
1809
|
+
eval_entry['data'] = d
|
|
1810
|
+
if 'output' in proto:
|
|
1811
|
+
eval_entry['output'] = proto['output']
|
|
1812
|
+
elif 'output' in metric:
|
|
1813
|
+
eval_entry['output'] = metric['output']
|
|
1814
|
+
evaluations[name] = eval_entry
|
|
1815
|
+
else:
|
|
1816
|
+
scalar_entry = deepcopy(metric)
|
|
1817
|
+
scalar_entry.pop('mode', None)
|
|
1818
|
+
scalar_custom[name] = scalar_entry
|
|
1819
|
+
|
|
1820
|
+
if scalar_custom:
|
|
1821
|
+
metrics_table['custom'] = scalar_custom
|
|
1822
|
+
if metrics_table:
|
|
1823
|
+
model_spec['metrics'] = metrics_table
|
|
1824
|
+
if evaluations:
|
|
1825
|
+
model_spec['evaluations'] = evaluations
|
|
1826
|
+
|
|
1827
|
+
return Model(model_spec, base=base)
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def describe(path=None, networks=False, write=False):
|
|
1831
|
+
"""Describe a model's formulation, losses, optimizers, and network architectures."""
|
|
1832
|
+
from .model_description import MODEL_FILE, build_networks, describe_run, load as load_model_desc
|
|
1833
|
+
manifest = {}
|
|
1834
|
+
config = None
|
|
1835
|
+
resolved_path = None
|
|
1836
|
+
|
|
1837
|
+
if isinstance(path, Model):
|
|
1838
|
+
config = resolve(path)
|
|
1839
|
+
manifest = {"config": config, "config_sha256": fingerprint(path)}
|
|
1840
|
+
elif isinstance(path, Run):
|
|
1841
|
+
resolved_path = path.path
|
|
1842
|
+
manifest, config = load_model_desc(resolved_path)
|
|
1843
|
+
elif isinstance(path, dict):
|
|
1844
|
+
if "components" in path or "networks" in path:
|
|
1845
|
+
config = path
|
|
1846
|
+
manifest = {"config": config}
|
|
1847
|
+
else:
|
|
1848
|
+
manifest, config = load_model_desc(path)
|
|
1849
|
+
else:
|
|
1850
|
+
if path is None or (not Path(path).exists() and not str(path).endswith(".toml")):
|
|
1851
|
+
from .registry import resolve_run_target
|
|
1852
|
+
try:
|
|
1853
|
+
resolved_path = resolve_run_target(path)
|
|
1854
|
+
except Exception:
|
|
1855
|
+
if path is None:
|
|
1856
|
+
raise
|
|
1857
|
+
resolved_path = Path(path)
|
|
1858
|
+
else:
|
|
1859
|
+
resolved_path = Path(path)
|
|
1860
|
+
manifest, config = load_model_desc(resolved_path)
|
|
1861
|
+
|
|
1862
|
+
if write and (resolved_path is None or not (resolved_path.is_dir() and manifest)):
|
|
1863
|
+
raise ValueError("--write needs a run directory")
|
|
1864
|
+
if not isinstance(config, dict):
|
|
1865
|
+
raise ValueError("No model configuration recorded for this run")
|
|
1866
|
+
recorded = None
|
|
1867
|
+
if networks or write:
|
|
1868
|
+
recorded = build_networks(config, manifest.get("config_sha256"))
|
|
1869
|
+
if write and resolved_path is not None:
|
|
1870
|
+
from .run_state import atomic_json
|
|
1871
|
+
atomic_json(resolved_path / MODEL_FILE, recorded)
|
|
1872
|
+
return describe_run(manifest if manifest else {"config": config}, recorded=recorded)
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
describe_model = describe
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def data_check(config, output=None):
|
|
1879
|
+
"""Inspect and inventory a dataset folder defined in config, optionally writing a manifest."""
|
|
1880
|
+
from .data import ImageFolder
|
|
1881
|
+
from .config import load_config
|
|
1882
|
+
cfg = resolve(config) if isinstance(config, Model) or is_model_file(config) else load_config(config)
|
|
1883
|
+
if cfg["data"]["factory"] != "image_folder":
|
|
1884
|
+
raise ValueError("data-check currently supports data.factory='image_folder'")
|
|
1885
|
+
data = ImageFolder(**cfg["data"]["args"])
|
|
1886
|
+
manifest = {"schema_version": 1, "data": data.resume_identity(), "inventory": data.inventory}
|
|
1887
|
+
if output is not None:
|
|
1888
|
+
out_path = Path(output)
|
|
1889
|
+
with out_path.open("x", encoding="utf-8") as stream:
|
|
1890
|
+
json.dump(manifest, stream, indent=2, allow_nan=False)
|
|
1891
|
+
stream.write("\n")
|
|
1892
|
+
return manifest
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
check_data = data_check
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def preflight(config, profile=None, runtime=False):
|
|
1899
|
+
"""Validate execution environment, hardware, and configuration compatibility."""
|
|
1900
|
+
from .execution_profiles import load_execution_profile
|
|
1901
|
+
from .execution_preflight import _resolve_profile, preflight as run_preflight
|
|
1902
|
+
from .config import load_config
|
|
1903
|
+
cfg = resolve(config) if isinstance(config, Model) or is_model_file(config) else load_config(config)
|
|
1904
|
+
prof = (load_execution_profile(profile, cfg) if profile is not None
|
|
1905
|
+
else _resolve_profile(None, cfg))
|
|
1906
|
+
if runtime:
|
|
1907
|
+
return run_preflight(cfg, prof)
|
|
1908
|
+
return {"schema_version": 1, "stage": "structural", "profile": prof, "runtime_checked": False}
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def preflight_check(config, profile=None, runtime=True):
|
|
1912
|
+
"""Run preflight check on config (defaults to runtime=True)."""
|
|
1913
|
+
return preflight(config, profile=profile, runtime=runtime)
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
def serve(run_dir=None, port=None, host="0.0.0.0", auth="none", session_file=None, open_browser=False, public_origin=None, dev=False):
|
|
1917
|
+
"""Launch the Web Observatory server for a runs root or run directory."""
|
|
1918
|
+
if dev:
|
|
1919
|
+
from .web_dev import enable
|
|
1920
|
+
enable()
|
|
1921
|
+
from .web_launch import serve as launch_serve
|
|
1922
|
+
target = find_runs_root(Path.cwd()) if run_dir is None else run_dir
|
|
1923
|
+
launch_serve(target, port=port, host=host, auth=auth,
|
|
1924
|
+
session_file=session_file, open_browser=open_browser,
|
|
1925
|
+
public_origin=public_origin)
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
def viewer_status(run_dir=None):
|
|
1929
|
+
"""Check the running status and port of a viewer process for a run or runs root."""
|
|
1930
|
+
from .web_autostart import viewer_status as autostart_status
|
|
1931
|
+
return autostart_status(_run_path(run_dir))
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def stop_viewer(run_dir=None):
|
|
1935
|
+
"""Stop a running viewer process for a run directory."""
|
|
1936
|
+
from .web_autostart import stop_viewer as autostart_stop
|
|
1937
|
+
return autostart_stop(_run_path(run_dir))
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def contributions(run_dir=None, map_revision=None, cursor=None, limit=100, max_bytes=1048576):
|
|
1941
|
+
"""Read a page of mapped contribution events, auto-projecting if needed."""
|
|
1942
|
+
from .event_views import MapSpec, read_projection_page
|
|
1943
|
+
run_path = _run_path(run_dir)
|
|
1944
|
+
rev = map_revision or MapSpec().revision
|
|
1945
|
+
view_dir = run_path / 'views' / rev
|
|
1946
|
+
has_events = (run_path / 'events.jsonl').is_file()
|
|
1947
|
+
has_views = (view_dir / 'contributions.jsonl').is_file() and (view_dir / 'metadata.json').is_file()
|
|
1948
|
+
if has_events and not has_views:
|
|
1949
|
+
project(run_path)
|
|
1950
|
+
return read_projection_page(run_path, rev, cursor, limit=limit, max_bytes=max_bytes)
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def project(run_dir=None, map_spec=None, follow=False, limit=100, on_progress=None):
|
|
1954
|
+
"""Project run events into the mapped contribution views."""
|
|
1955
|
+
from .event_views import MapSpec, Projector
|
|
1956
|
+
import time
|
|
1957
|
+
run_path = _run_path(run_dir)
|
|
1958
|
+
spec = MapSpec(**json.loads(Path(map_spec).read_text(encoding="utf-8"))) if map_spec else MapSpec()
|
|
1959
|
+
following = True
|
|
1960
|
+
last_progress = None
|
|
1961
|
+
while following:
|
|
1962
|
+
opened = time.monotonic()
|
|
1963
|
+
with Projector(run_path, spec) as projector:
|
|
1964
|
+
while True:
|
|
1965
|
+
progress = projector.project(limit=limit)
|
|
1966
|
+
last_progress = dict(progress, map_revision=spec.revision)
|
|
1967
|
+
if follow and progress["documents"] and on_progress:
|
|
1968
|
+
on_progress(last_progress)
|
|
1969
|
+
if not follow and not progress["has_more"]:
|
|
1970
|
+
following = False
|
|
1971
|
+
return last_progress
|
|
1972
|
+
if time.monotonic() - opened >= 240:
|
|
1973
|
+
break
|
|
1974
|
+
if not progress["has_more"]:
|
|
1975
|
+
time.sleep(0.25)
|
|
1976
|
+
return last_progress
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def checkpoint_status(run_dir=None, request_id=None):
|
|
1980
|
+
"""Check the status of an asynchronous checkpoint request."""
|
|
1981
|
+
from .run_requests import checkpoint_request_status
|
|
1982
|
+
return checkpoint_request_status(_run_path(run_dir), request_id)
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
def request_checkpoint(run_dir=None, attempt_id=None, request_id=None):
|
|
1986
|
+
"""Submit an asynchronous request for the training process to save a checkpoint."""
|
|
1987
|
+
from .run_requests import submit_checkpoint_request
|
|
1988
|
+
run_dir = _run_path(run_dir)
|
|
1989
|
+
manifest_path = run_dir / "manifest.json"
|
|
1990
|
+
if not manifest_path.is_file():
|
|
1991
|
+
from .inspection import check_run_active
|
|
1992
|
+
if check_run_active(run_dir)[0]:
|
|
1993
|
+
raise ValueError("Run is still initializing step 0; checkpoint will be available once step 1 starts")
|
|
1994
|
+
raise FileNotFoundError(f"Run manifest not found: {manifest_path}")
|
|
1995
|
+
with manifest_path.open(encoding="utf-8") as stream:
|
|
1996
|
+
manifest = json.load(stream)
|
|
1997
|
+
if not isinstance(manifest, dict) or not manifest.get("run_id") or not manifest.get("attempt_id"):
|
|
1998
|
+
from .inspection import check_run_active
|
|
1999
|
+
if check_run_active(run_dir)[0] or (isinstance(manifest, dict) and manifest.get("status") == "initializing"):
|
|
2000
|
+
raise ValueError("Run is still initializing step 0; checkpoint will be available once step 1 starts")
|
|
2001
|
+
raise ValueError("Run manifest has no run/attempt identity for a checkpoint request")
|
|
2002
|
+
if manifest.get("status") == "initializing":
|
|
2003
|
+
raise ValueError("Run is still initializing step 0; checkpoint will be available once step 1 starts")
|
|
2004
|
+
if manifest.get("status") != "running":
|
|
2005
|
+
from .inspection import check_run_active
|
|
2006
|
+
if check_run_active(run_dir)[0] and int(manifest.get("steps", 0) or 0) == 0:
|
|
2007
|
+
raise ValueError("Run is still initializing step 0; checkpoint will be available once step 1 starts")
|
|
2008
|
+
if request_id is None:
|
|
2009
|
+
raise ValueError("Run is not running; use resume to continue a stopped run")
|
|
2010
|
+
try:
|
|
2011
|
+
return checkpoint_status(run_dir, request_id)
|
|
2012
|
+
except FileNotFoundError as exc:
|
|
2013
|
+
raise ValueError("Run is not running; use resume to continue a stopped run") from exc
|
|
2014
|
+
return submit_checkpoint_request(run_dir, run_id=manifest["run_id"],
|
|
2015
|
+
attempt_id=attempt_id or manifest["attempt_id"],
|
|
2016
|
+
request_id=request_id)
|
|
2017
|
+
|
|
2018
|
+
|
|
2019
|
+
def ensure_dashboard_running(runs_root=None, check_version=True, run_id=None):
|
|
2020
|
+
"""Ensure the persistent root dashboard is running for runs_root, returning its URL."""
|
|
2021
|
+
from .web_autostart import ensure_dashboard_running as autostart_ensure
|
|
2022
|
+
target = Path(runs_root) if runs_root is not None else None
|
|
2023
|
+
return autostart_ensure(target, check_version=check_version, run_id=run_id)
|
|
2024
|
+
|
|
2025
|
+
|
|
2026
|
+
def stop_all_servers(force=False):
|
|
2027
|
+
"""Terminate all background viewer, dashboard, and supervisor processes on the host."""
|
|
2028
|
+
from .web_autostart import stop_all_servers as autostart_stop_all
|
|
2029
|
+
return autostart_stop_all(force=force)
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
def dashboard_status(runs_root=None):
|
|
2033
|
+
"""Query the status of the persistent Web Observatory dashboard for a runs root or run."""
|
|
2034
|
+
from .web_autostart import dashboard_status as autostart_dstatus
|
|
2035
|
+
target = Path(runs_root) if runs_root is not None else None
|
|
2036
|
+
return autostart_dstatus(target)
|
|
2037
|
+
|
|
2038
|
+
|
|
2039
|
+
def stop_dashboard(runs_root=None, timeout=8.0):
|
|
2040
|
+
"""Stop the persistent Web Observatory dashboard for a runs root or run."""
|
|
2041
|
+
from .web_autostart import stop_dashboard as autostart_sstop
|
|
2042
|
+
target = Path(runs_root) if runs_root is not None else None
|
|
2043
|
+
return autostart_sstop(target, timeout=timeout)
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
def find_runs_root(path=None):
|
|
2047
|
+
"""Locate the root directory containing training runs for a workspace or path."""
|
|
2048
|
+
from .web_autostart import find_runs_root as autostart_find
|
|
2049
|
+
return autostart_find(path)
|
|
2050
|
+
|
|
2051
|
+
|
|
2052
|
+
def stop_projector(run_dir):
|
|
2053
|
+
"""Stop the background projection worker for a run directory."""
|
|
2054
|
+
from .web_autostart import stop_projector as autostart_sproj
|
|
2055
|
+
return autostart_sproj(Path(run_dir))
|
|
2056
|
+
|
|
2057
|
+
|
|
2058
|
+
# --------------------------------------------------------------------------- job queue
|
|
2059
|
+
|
|
2060
|
+
def queue_add(model_file, devices=None, run_dir=None, name=None, steps=None, sets=None, no_web=False, wait=False, queue_file=None, **kwargs):
|
|
2061
|
+
"""Enqueue a training experiment or wait directive into the job queue."""
|
|
2062
|
+
from .queue import queue_add as _add
|
|
2063
|
+
return _add(model_file, devices=devices, run_dir=run_dir, name=name, steps=steps, sets=sets, no_web=no_web, wait=wait, queue_file=queue_file, **kwargs)
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
def queue_ls(status=None, queue_file=None):
|
|
2067
|
+
"""List queued, running, and completed jobs from the job queue."""
|
|
2068
|
+
from .queue import queue_ls as _ls
|
|
2069
|
+
return _ls(status_filter=status, queue_file=queue_file)
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
def queue_status(job_id=None, queue_file=None, lines=20):
|
|
2073
|
+
"""Check the execution status and recent logs of a queued job."""
|
|
2074
|
+
from .queue import queue_status as _status
|
|
2075
|
+
return _status(job_id=job_id, queue_file=queue_file, lines=lines)
|
|
2076
|
+
|
|
2077
|
+
|
|
2078
|
+
def queue_stop(job_id=None, stop_all=False, queue_file=None):
|
|
2079
|
+
"""Stop or cancel a queued job, or all jobs if stop_all=True."""
|
|
2080
|
+
from .queue import queue_stop as _stop
|
|
2081
|
+
return _stop(job_id=job_id, stop_all=stop_all, queue_file=queue_file)
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def queue_run(devices=None, poll_interval=5.0, once=False, queue_file=None):
|
|
2085
|
+
"""Process and run jobs sequentially from the queue until drained."""
|
|
2086
|
+
from .queue import queue_run as _run
|
|
2087
|
+
return _run(devices=devices, poll_interval=poll_interval, once=once, queue_file=queue_file)
|
|
2088
|
+
|
|
2089
|
+
|
|
2090
|
+
def queue_install_service(user=True, devices=None, name=None, dry_run=False):
|
|
2091
|
+
"""Generate or install a systemd service unit for the HyperGAN queue runner."""
|
|
2092
|
+
from .queue import install_service as _install
|
|
2093
|
+
return _install(user=user, devices=devices, name=name, dry_run=dry_run)
|
|
2094
|
+
|
|
2095
|
+
|
|
2096
|
+
def queue_clear(queue_file=None):
|
|
2097
|
+
"""Clear all queued and running jobs in the job queue."""
|
|
2098
|
+
from .queue import queue_clear as _clear
|
|
2099
|
+
return _clear(queue_file=queue_file)
|
|
2100
|
+
|
|
2101
|
+
|
|
2102
|
+
# --------------------------------------------------------------------------- convenience exports
|
|
2103
|
+
|
|
2104
|
+
def compare(runs, metric="fid50k_train", at_steps=None):
|
|
2105
|
+
"""Compare multiple runs for a given metric across step milestones."""
|
|
2106
|
+
return runs_compare(runs, metric=metric, at_steps=at_steps)
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
def _clean_diff_dict(cfg):
|
|
2110
|
+
if not isinstance(cfg, dict):
|
|
2111
|
+
return cfg
|
|
2112
|
+
cleaned = deepcopy(cfg)
|
|
2113
|
+
for k in ("warnings", "qualification", "_source_path", "name_map", "schema_version", "format"):
|
|
2114
|
+
cleaned.pop(k, None)
|
|
2115
|
+
if isinstance(cleaned.get("recipe"), dict):
|
|
2116
|
+
cleaned.pop("recipe", None)
|
|
2117
|
+
return cleaned
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
def _extract_diff_config(target):
|
|
2121
|
+
if isinstance(target, Model):
|
|
2122
|
+
return _clean_diff_dict(dict(resolve(target)))
|
|
2123
|
+
if isinstance(target, Run):
|
|
2124
|
+
if target.manifest.get("config") and isinstance(target.manifest["config"], dict):
|
|
2125
|
+
return _clean_diff_dict(target.manifest["config"])
|
|
2126
|
+
target = target.path
|
|
2127
|
+
if isinstance(target, dict):
|
|
2128
|
+
if "components" in target or "networks" in target:
|
|
2129
|
+
return _clean_diff_dict(dict(resolve(from_dict(target))))
|
|
2130
|
+
if "config" in target and isinstance(target["config"], dict):
|
|
2131
|
+
return _clean_diff_dict(target["config"])
|
|
2132
|
+
return _clean_diff_dict(target)
|
|
2133
|
+
target_str = str(target)
|
|
2134
|
+
target_p = Path(target_str)
|
|
2135
|
+
if target_str.endswith(".toml") or (target_p.is_file() and target_p.suffix == ".toml"):
|
|
2136
|
+
if not target_p.is_file():
|
|
2137
|
+
raise FileNotFoundError(f"Recipe target '{target_str}' not found")
|
|
2138
|
+
m = load(target_p)
|
|
2139
|
+
return _clean_diff_dict(dict(resolve(m)))
|
|
2140
|
+
if target_str.endswith(".pt") or (target_p.is_file() and target_p.suffix == ".pt"):
|
|
2141
|
+
if not target_p.is_file():
|
|
2142
|
+
raise FileNotFoundError(f"Checkpoint file '{target_str}' not found")
|
|
2143
|
+
import torch
|
|
2144
|
+
state = torch.load(target_p, map_location="cpu", weights_only=True)
|
|
2145
|
+
if isinstance(state, dict):
|
|
2146
|
+
if "config" in state and isinstance(state["config"], dict):
|
|
2147
|
+
return _clean_diff_dict(state["config"])
|
|
2148
|
+
if "model_state_dict" in state:
|
|
2149
|
+
return {k: list(v.shape) for k, v in state["model_state_dict"].items() if hasattr(v, "shape")}
|
|
2150
|
+
return {}
|
|
2151
|
+
resolved = resolve_run_target(target_str)
|
|
2152
|
+
manifest_p = resolved / "manifest.json"
|
|
2153
|
+
if manifest_p.is_file():
|
|
2154
|
+
try:
|
|
2155
|
+
m_data = json.loads(manifest_p.read_text(encoding="utf-8"))
|
|
2156
|
+
if "config" in m_data and isinstance(m_data["config"], dict):
|
|
2157
|
+
return _clean_diff_dict(m_data["config"])
|
|
2158
|
+
except Exception:
|
|
2159
|
+
pass
|
|
2160
|
+
for fname in ("model.toml", "recipe.toml", "config.toml"):
|
|
2161
|
+
recipe_p = resolved / fname
|
|
2162
|
+
if recipe_p.is_file():
|
|
2163
|
+
m = load(recipe_p)
|
|
2164
|
+
return _clean_diff_dict(dict(resolve(m)))
|
|
2165
|
+
raise FileNotFoundError(f"Run or recipe target '{target_str}' not found or contains no configuration")
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
def _flatten_diff_dict(d, prefix=""):
|
|
2169
|
+
flat = {}
|
|
2170
|
+
if isinstance(d, dict):
|
|
2171
|
+
for k, v in d.items():
|
|
2172
|
+
full = f"{prefix}.{k}" if prefix else str(k)
|
|
2173
|
+
if isinstance(v, dict) and v:
|
|
2174
|
+
flat.update(_flatten_diff_dict(v, full))
|
|
2175
|
+
else:
|
|
2176
|
+
flat[full] = v
|
|
2177
|
+
else:
|
|
2178
|
+
flat[prefix] = d
|
|
2179
|
+
return flat
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
def diff(target_a, target_b=None, *, sets=None, overrides=None):
|
|
2183
|
+
"""Compare two configurations, models, runs, or checkpoints, returning differences."""
|
|
2184
|
+
merged_sets = dict(sets or {})
|
|
2185
|
+
if overrides:
|
|
2186
|
+
merged_sets.update(overrides)
|
|
2187
|
+
if target_b is None:
|
|
2188
|
+
if not merged_sets:
|
|
2189
|
+
raise ValueError("diff requires target_b or overrides/sets")
|
|
2190
|
+
m_b = override(_model(target_a), merged_sets)
|
|
2191
|
+
target_b_label = f"{getattr(target_a, 'path', target_a)} (with overrides)"
|
|
2192
|
+
cfg_b = _extract_diff_config(m_b)
|
|
2193
|
+
elif merged_sets:
|
|
2194
|
+
m_b = override(_model(target_b), merged_sets)
|
|
2195
|
+
target_b_label = str(getattr(target_b, "path", target_b))
|
|
2196
|
+
cfg_b = _extract_diff_config(m_b)
|
|
2197
|
+
else:
|
|
2198
|
+
target_b_label = str(getattr(target_b, "path", target_b))
|
|
2199
|
+
cfg_b = _extract_diff_config(target_b)
|
|
2200
|
+
cfg_a = _extract_diff_config(target_a)
|
|
2201
|
+
flat_a = _flatten_diff_dict(cfg_a)
|
|
2202
|
+
flat_b = _flatten_diff_dict(cfg_b)
|
|
2203
|
+
all_keys = sorted(set(flat_a.keys()) | set(flat_b.keys()))
|
|
2204
|
+
differences = {}
|
|
2205
|
+
for k in all_keys:
|
|
2206
|
+
val_a = flat_a.get(k)
|
|
2207
|
+
val_b = flat_b.get(k)
|
|
2208
|
+
if val_a != val_b:
|
|
2209
|
+
differences[k] = {"a": val_a, "b": val_b}
|
|
2210
|
+
return {
|
|
2211
|
+
"target_a": str(getattr(target_a, "path", target_a)),
|
|
2212
|
+
"target_b": target_b_label,
|
|
2213
|
+
"identical": len(differences) == 0,
|
|
2214
|
+
"differences": differences,
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
|
|
2218
|
+
def list_templates():
|
|
2219
|
+
"""List available starter templates (e.g. gaussian-grid, dcgan, resnet, tiny-transformer)."""
|
|
2220
|
+
from .recipe_generator import list_starter_templates
|
|
2221
|
+
return list_starter_templates()
|