expr-tracker 0.2.4__tar.gz → 0.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/.github/workflows/docs.yaml +1 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/PKG-INFO +23 -7
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/README.md +22 -3
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/design.md +6 -3
- expr_tracker-0.2.6/docs/examples.md +10 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/getting-started.md +8 -1
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/alerts.md +2 -2
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/cli.md +2 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/distributed.md +25 -2
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/history.md +8 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/streams.md +33 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/index.md +2 -1
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/reference/api.md +10 -0
- expr_tracker-0.2.6/examples/README.md +65 -0
- expr_tracker-0.2.6/examples/alert_rules.py +115 -0
- expr_tracker-0.2.6/examples/checkpoints.py +83 -0
- expr_tracker-0.2.6/examples/early_stopping.py +99 -0
- expr_tracker-0.2.6/examples/multiprocess_pipeline.py +205 -0
- expr_tracker-0.2.6/examples/profile_step.py +92 -0
- expr_tracker-0.2.6/examples/quickstart.py +90 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/mkdocs.yml +4 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/pyproject.toml +0 -3
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/backends/__init__.py +34 -38
- expr_tracker-0.2.6/src/expr_tracker/alerts/backends/cards.py +102 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/cli.py +14 -1
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/run.py +20 -6
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_alert_backends.py +24 -21
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_cli.py +43 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_distributed.py +106 -0
- expr_tracker-0.2.6/tests/test_examples.py +383 -0
- expr_tracker-0.2.6/tests/test_lark.py +380 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/uv.lock +1 -61
- expr_tracker-0.2.4/tests/test_lark_live.py +0 -269
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/.github/workflows/release.yaml +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/.gitignore +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/LICENSE +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/architecture.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/artifacts.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/backends.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/logging.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/guide/spans.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/reference/configuration.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/docs/reference/expressions.md +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/__init__.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/_compat.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/__init__.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/backends/base.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/dispatch.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/engine.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/__init__.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/eval.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/functions.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/lexer.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/nodes.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/parser.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/expr/rule.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/alerts/models.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/artifacts.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/encoders.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/__init__.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/codec.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/frame.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/naming.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/reader.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/series.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/store.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/history/writer.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/plugins.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/py.typed +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/spans.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/summary.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/trace.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/tracker.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/src/expr_tracker/types.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/conftest.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_alert_delivery.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_alert_dispatch.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_alert_engine.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_alert_models.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_alert_routing.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_artifacts.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_benchmark.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_cache.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_correctness.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_e2e.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_expr_builder.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_expr_eval.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_expr_functions.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_expr_parser.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_expr_properties.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_failure_modes.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_features.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_frame_codec_summary.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_history.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_hot_paths.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_integration.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_perf.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_public_surfaces.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_review_regressions.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_rule_lifecycle.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_run_backends.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_scenarios.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_span_plugins.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_spans.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_step_commit.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_streams.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_stress.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_trace.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_trackio.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_value_encoding.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_wandb.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_writer_buffer.py +0 -0
- {expr_tracker-0.2.4 → expr_tracker-0.2.6}/tests/test_writer_durability.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: expr_tracker
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Local-first experiment tracking with queryable history and expression-based alerts on your training metrics
|
|
5
5
|
Project-URL: Homepage, https://hspk.github.io/expr_tracker/
|
|
6
6
|
Project-URL: Documentation, https://hspk.github.io/expr_tracker/
|
|
@@ -31,13 +31,10 @@ Provides-Extra: all
|
|
|
31
31
|
Requires-Dist: nvidia-ml-py>=12.0; extra == 'all'
|
|
32
32
|
Requires-Dist: pandas>=1.5; extra == 'all'
|
|
33
33
|
Requires-Dist: polars>=0.20; extra == 'all'
|
|
34
|
-
Requires-Dist: slark>=0.1.28; extra == 'all'
|
|
35
34
|
Requires-Dist: trackio>=0.4.0; extra == 'all'
|
|
36
35
|
Requires-Dist: wandb>=0.21.0; extra == 'all'
|
|
37
36
|
Provides-Extra: gpu
|
|
38
37
|
Requires-Dist: nvidia-ml-py>=12.0; extra == 'gpu'
|
|
39
|
-
Provides-Extra: lark
|
|
40
|
-
Requires-Dist: slark>=0.1.28; extra == 'lark'
|
|
41
38
|
Provides-Extra: pandas
|
|
42
39
|
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
43
40
|
Provides-Extra: polars
|
|
@@ -92,7 +89,6 @@ et.history(-1, output_type="pd") # everything, as a DataFrame
|
|
|
92
89
|
uv add expr_tracker # local-first: click, loguru, pydantic only
|
|
93
90
|
uv add "expr_tracker[wandb]" # mirror to Weights & Biases
|
|
94
91
|
uv add "expr_tracker[trackio]" # mirror to trackio
|
|
95
|
-
uv add "expr_tracker[lark]" # Feishu/Lark alert channel
|
|
96
92
|
uv add "expr_tracker[pandas]" # history(output_type="pandas")
|
|
97
93
|
uv add "expr_tracker[all]" # everything
|
|
98
94
|
```
|
|
@@ -112,7 +108,26 @@ install command; it never crashes a run.
|
|
|
112
108
|
| [Spans](https://hspk.github.io/expr_tracker/guide/spans/) | Time the parts of a step, and their parts. Each duration becomes a metric, so alerts and queries work on it unchanged; `et trace` exports the timeline for Perfetto. `print_fn` prints the tree live, and plugins attach CPU and GPU cost to each region. |
|
|
113
109
|
| [Streams](https://hspk.github.io/expr_tracker/guide/streams/) | Independent producers — a data worker and a training loop — each with their own step cursor and file inside one run. |
|
|
114
110
|
| [Distributed](https://hspk.github.io/expr_tracker/guide/distributed/) | Per-rank shards so concurrent appends cannot corrupt step order; only rank 0 alerts by default. |
|
|
115
|
-
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et rules explain`, `et rules test`, `et alert`. |
|
|
111
|
+
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et trace`, `et rules explain`, `et rules test`, `et alert`. |
|
|
112
|
+
|
|
113
|
+
## Examples
|
|
114
|
+
|
|
115
|
+
Every one runs offline, with no account and no network.
|
|
116
|
+
|
|
117
|
+
| | |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| [`quickstart.py`](examples/quickstart.py) | The sixty-second tour: log a run, merge eval into the training step, read the history back while it is open and again afterwards. |
|
|
120
|
+
| [`alert_rules.py`](examples/alert_rules.py) | Four rules against four faults — a loss spike, a non-finite loss, a stalled curve and a regression that must persist. `--fault none` fires nothing, which is the point. |
|
|
121
|
+
| [`profile_step.py`](examples/profile_step.py) | Where a step goes. Nested spans become metrics, a plugin adds CPU cost, and the tree exports to Perfetto. |
|
|
122
|
+
| [`early_stopping.py`](examples/early_stopping.py) | Querying your own history mid-run to decay the learning rate on a plateau and stop when it stops paying. |
|
|
123
|
+
| [`checkpoints.py`](examples/checkpoints.py) | Checkpoints as versioned artifacts, deduplicated by content and fetched later by alias. |
|
|
124
|
+
| [`multiprocess_pipeline.py`](examples/multiprocess_pipeline.py) | Four data producers and four trainers as eight processes in one run, with bounded staleness. Each worker gets its own stream and its own lane in the trace, and the blocking spans show which side is the bottleneck. |
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uv run python examples/quickstart.py
|
|
128
|
+
uv run python examples/alert_rules.py --fault spike
|
|
129
|
+
uv run python examples/multiprocess_pipeline.py --produce-ms 10 --train-ms 40
|
|
130
|
+
```
|
|
116
131
|
|
|
117
132
|
## wandb compatibility
|
|
118
133
|
|
|
@@ -155,12 +170,13 @@ uv run ruff format src tests
|
|
|
155
170
|
| `test_expr_properties.py` | DSL properties: render round-trip stability, precedence, the whole `M` builder |
|
|
156
171
|
| `test_trace.py` | Chrome Trace export: lane layout, stream and step selection, the CLI |
|
|
157
172
|
| `test_spans.py` | nesting, aggregation, decorator and async forms, errors, thread and task isolation |
|
|
173
|
+
| `test_examples.py` | the shipped examples run, and their backpressure claims hold |
|
|
158
174
|
| `test_span_plugins.py` | `print_fn` output and indentation, the plugin protocol, failure isolation, CPU/GPU built-ins |
|
|
159
175
|
| `test_streams.py` | stream naming and validation, isolation, resolution order, backend grouping, two-process runs |
|
|
160
176
|
| `test_distributed.py` | rank shards, `alert_on_rank`, real multi-process runs |
|
|
161
177
|
| `test_wandb.py` | real wandb in offline mode: parameter mapping, step alignment, artifacts |
|
|
162
178
|
| `test_trackio.py` | trackio contract, resume mapping, real end-to-end |
|
|
163
|
-
| `
|
|
179
|
+
| `test_lark.py` | Lark card construction; real delivery when `ET_LARK_TEST_WEBHOOK` is set |
|
|
164
180
|
| `test_stress.py` (`slow`) | 100k-row writes, concurrency, cache thrash, write-failure recovery |
|
|
165
181
|
| `test_benchmark.py` (`benchmark`) | throughput, tail latency, query cost, memory stability |
|
|
166
182
|
|
|
@@ -42,7 +42,6 @@ et.history(-1, output_type="pd") # everything, as a DataFrame
|
|
|
42
42
|
uv add expr_tracker # local-first: click, loguru, pydantic only
|
|
43
43
|
uv add "expr_tracker[wandb]" # mirror to Weights & Biases
|
|
44
44
|
uv add "expr_tracker[trackio]" # mirror to trackio
|
|
45
|
-
uv add "expr_tracker[lark]" # Feishu/Lark alert channel
|
|
46
45
|
uv add "expr_tracker[pandas]" # history(output_type="pandas")
|
|
47
46
|
uv add "expr_tracker[all]" # everything
|
|
48
47
|
```
|
|
@@ -62,7 +61,26 @@ install command; it never crashes a run.
|
|
|
62
61
|
| [Spans](https://hspk.github.io/expr_tracker/guide/spans/) | Time the parts of a step, and their parts. Each duration becomes a metric, so alerts and queries work on it unchanged; `et trace` exports the timeline for Perfetto. `print_fn` prints the tree live, and plugins attach CPU and GPU cost to each region. |
|
|
63
62
|
| [Streams](https://hspk.github.io/expr_tracker/guide/streams/) | Independent producers — a data worker and a training loop — each with their own step cursor and file inside one run. |
|
|
64
63
|
| [Distributed](https://hspk.github.io/expr_tracker/guide/distributed/) | Per-rank shards so concurrent appends cannot corrupt step order; only rank 0 alerts by default. |
|
|
65
|
-
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et rules explain`, `et rules test`, `et alert`. |
|
|
64
|
+
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et trace`, `et rules explain`, `et rules test`, `et alert`. |
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
Every one runs offline, with no account and no network.
|
|
69
|
+
|
|
70
|
+
| | |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| [`quickstart.py`](examples/quickstart.py) | The sixty-second tour: log a run, merge eval into the training step, read the history back while it is open and again afterwards. |
|
|
73
|
+
| [`alert_rules.py`](examples/alert_rules.py) | Four rules against four faults — a loss spike, a non-finite loss, a stalled curve and a regression that must persist. `--fault none` fires nothing, which is the point. |
|
|
74
|
+
| [`profile_step.py`](examples/profile_step.py) | Where a step goes. Nested spans become metrics, a plugin adds CPU cost, and the tree exports to Perfetto. |
|
|
75
|
+
| [`early_stopping.py`](examples/early_stopping.py) | Querying your own history mid-run to decay the learning rate on a plateau and stop when it stops paying. |
|
|
76
|
+
| [`checkpoints.py`](examples/checkpoints.py) | Checkpoints as versioned artifacts, deduplicated by content and fetched later by alias. |
|
|
77
|
+
| [`multiprocess_pipeline.py`](examples/multiprocess_pipeline.py) | Four data producers and four trainers as eight processes in one run, with bounded staleness. Each worker gets its own stream and its own lane in the trace, and the blocking spans show which side is the bottleneck. |
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv run python examples/quickstart.py
|
|
81
|
+
uv run python examples/alert_rules.py --fault spike
|
|
82
|
+
uv run python examples/multiprocess_pipeline.py --produce-ms 10 --train-ms 40
|
|
83
|
+
```
|
|
66
84
|
|
|
67
85
|
## wandb compatibility
|
|
68
86
|
|
|
@@ -105,12 +123,13 @@ uv run ruff format src tests
|
|
|
105
123
|
| `test_expr_properties.py` | DSL properties: render round-trip stability, precedence, the whole `M` builder |
|
|
106
124
|
| `test_trace.py` | Chrome Trace export: lane layout, stream and step selection, the CLI |
|
|
107
125
|
| `test_spans.py` | nesting, aggregation, decorator and async forms, errors, thread and task isolation |
|
|
126
|
+
| `test_examples.py` | the shipped examples run, and their backpressure claims hold |
|
|
108
127
|
| `test_span_plugins.py` | `print_fn` output and indentation, the plugin protocol, failure isolation, CPU/GPU built-ins |
|
|
109
128
|
| `test_streams.py` | stream naming and validation, isolation, resolution order, backend grouping, two-process runs |
|
|
110
129
|
| `test_distributed.py` | rank shards, `alert_on_rank`, real multi-process runs |
|
|
111
130
|
| `test_wandb.py` | real wandb in offline mode: parameter mapping, step alignment, artifacts |
|
|
112
131
|
| `test_trackio.py` | trackio contract, resume mapping, real end-to-end |
|
|
113
|
-
| `
|
|
132
|
+
| `test_lark.py` | Lark card construction; real delivery when `ET_LARK_TEST_WEBHOOK` is set |
|
|
114
133
|
| `test_stress.py` (`slow`) | 100k-row writes, concurrency, cache thrash, write-failure recovery |
|
|
115
134
|
| `test_benchmark.py` (`benchmark`) | throughput, tail latency, query cost, memory stability |
|
|
116
135
|
|
|
@@ -375,9 +375,12 @@ AlertRule(name, condition, level="warning", title=None,
|
|
|
375
375
|
timeout.
|
|
376
376
|
- **Only send failures are swallowed**; configuration errors raise at configuration
|
|
377
377
|
time.
|
|
378
|
-
- **Backends**: `lark
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
- **Backends**: `lark`, `slack`, `dingtalk`, `wecom` and `webhook` (a generic
|
|
379
|
+
template) use stdlib `urllib`; `email` uses stdlib `smtplib` — **every channel is
|
|
380
|
+
dependency-free**. `register_backend()` extends the set. The Lark card layout is
|
|
381
|
+
built as plain dicts in `backends/cards.py` rather than pulled in with a client
|
|
382
|
+
library: it is one JSON body, and a whole HTTP stack to shape it was not a trade
|
|
383
|
+
worth making.
|
|
381
384
|
- **Configuration precedence**: `init(alert=)` > `configure_alert()` >
|
|
382
385
|
`ET_ALERT_CONFIG` file > environment (`ET_LARK_WEBHOOK_URL`, legacy `WEBHOOK_URL`)
|
|
383
386
|
> defaults.
|
|
@@ -12,10 +12,13 @@ Only `click`, `loguru` and `pydantic` are required. Everything else is an extra:
|
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| `wandb` | mirror metrics to Weights & Biases |
|
|
14
14
|
| `trackio` | mirror metrics to trackio |
|
|
15
|
-
| `lark` | Feishu/Lark alert channel |
|
|
16
15
|
| `pandas` / `polars` | `history(output_type=...)` frames |
|
|
16
|
+
| `gpu` | `GpuStats` span plugin, via NVML |
|
|
17
17
|
| `all` | all of the above |
|
|
18
18
|
|
|
19
|
+
Alert channels need nothing: every one of them, Lark included, is built on the
|
|
20
|
+
standard library.
|
|
21
|
+
|
|
19
22
|
A missing extra is reported with the exact install command; it never crashes a run.
|
|
20
23
|
|
|
21
24
|
## A complete run
|
|
@@ -85,12 +88,16 @@ et.get_run().step # 1000 - the cursor picked up where it left off
|
|
|
85
88
|
len(et.history(-1)) # 1000 - the old rows are still there
|
|
86
89
|
```
|
|
87
90
|
|
|
91
|
+
This is unconditional: `resume` is passed on to wandb and trackio, but the local
|
|
92
|
+
file always continues. Use a different name to start clean.
|
|
93
|
+
|
|
88
94
|
A process that dies without `finish()` still leaves a complete, valid file: the open
|
|
89
95
|
row is committed and the summary saved by an exit hook, and a torn trailing line is
|
|
90
96
|
repaired on the next run.
|
|
91
97
|
|
|
92
98
|
## Next
|
|
93
99
|
|
|
100
|
+
- [Examples](examples.md) — six runnable programs, all offline.
|
|
94
101
|
- [Logging metrics](guide/logging.md) for commit semantics and out-of-order steps.
|
|
95
102
|
- [Alerts](guide/alerts.md) to get notified instead of watching curves.
|
|
96
103
|
- [CLI](guide/cli.md) to inspect runs without writing code.
|
|
@@ -90,8 +90,8 @@ et.alert(title="done", text="training finished", level="info", channels=["oncall
|
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
Built-in types: `lark`, `slack`, `dingtalk`, `wecom`, `webhook` (a generic JSON
|
|
93
|
-
template), `email`, `callable`. All
|
|
94
|
-
your own with `register_backend()`.
|
|
93
|
+
template), `email`, `callable`. All of them use only the standard library, so no
|
|
94
|
+
channel needs an extra. Add your own with `register_backend()`.
|
|
95
95
|
|
|
96
96
|
### Email
|
|
97
97
|
|
|
@@ -11,6 +11,7 @@ et history tracker/jsonl/demo/run-1
|
|
|
11
11
|
et history tracker/jsonl/demo/run-1 -n 50 --metrics loss,lr
|
|
12
12
|
et history tracker/jsonl/demo/run-1 --step-range 100:200 --format json
|
|
13
13
|
et history tracker/jsonl/demo/run-1 -n -1 --format csv > run.csv
|
|
14
|
+
et history runs/llm/sft-1 --stream data
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
| Option | Default | Meaning |
|
|
@@ -18,6 +19,7 @@ et history tracker/jsonl/demo/run-1 -n -1 --format csv > run.csv
|
|
|
18
19
|
| `-n` | 20 | number of steps, `-1` for all |
|
|
19
20
|
| `--metrics` | all | comma-separated names |
|
|
20
21
|
| `--step-range` | — | `start:end`, end exclusive; either side may be empty |
|
|
22
|
+
| `--stream` | the unnamed producer | which [stream](streams.md) to read |
|
|
21
23
|
| `--format` | `table` | `table`, `json` or `csv` |
|
|
22
24
|
|
|
23
25
|
The argument is a run directory or a `metrics.jsonl` file.
|
|
@@ -50,6 +50,29 @@ its rules, and reports them through `info()`:
|
|
|
50
50
|
et.info()["rank"] # which rank this process was detected as
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## Remote backends
|
|
54
|
+
|
|
55
|
+
wandb and trackio identify a run by id, and neither can merge two step axes into
|
|
56
|
+
one run. If every rank opened the same id they would interleave their steps into
|
|
57
|
+
it — exactly what the local shards exist to prevent. So only rank 0 opens a
|
|
58
|
+
remote run by default:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
et.init(..., backends=["wandb"]) # rank 0 reports (default)
|
|
62
|
+
et.init(..., backends=["wandb"], backend_on_rank=2) # rank 2 reports instead
|
|
63
|
+
et.init(..., backends=["wandb"], backend_on_rank=None) # every rank reports
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A silenced rank still writes its full local history; it simply never calls the
|
|
67
|
+
backend. When every rank does report, each gets its own run id and they are tied
|
|
68
|
+
together with `group`, which both backends understand:
|
|
69
|
+
|
|
70
|
+
| | id | group |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| rank 0 | `sft-1` | `sft-1` |
|
|
73
|
+
| rank 2 | `sft-1-rank2` | `sft-1` |
|
|
74
|
+
| rank 2 of stream `data` | `sft-1-data-rank2` | `sft-1` |
|
|
75
|
+
|
|
53
76
|
## Typical setup
|
|
54
77
|
|
|
55
78
|
```python
|
|
@@ -64,5 +87,5 @@ et.init(
|
|
|
64
87
|
)
|
|
65
88
|
```
|
|
66
89
|
|
|
67
|
-
Every rank writes to `/shared/runs/llm/sft-<id>/`, each into its own shard
|
|
68
|
-
rank 0 pages you.
|
|
90
|
+
Every rank writes to `/shared/runs/llm/sft-<id>/`, each into its own shard. Only
|
|
91
|
+
rank 0 pages you, and only rank 0 opens the wandb run.
|
|
@@ -30,6 +30,14 @@ et.history(-1, metrics=["eval/acc"], dropna=True) # drop steps without it
|
|
|
30
30
|
`dropna` drops a row when **all** selected metrics are missing, which is what you
|
|
31
31
|
want for sparse eval metrics logged every N steps.
|
|
32
32
|
|
|
33
|
+
`n` selects rows first and `dropna` filters them afterwards. Asking for the last
|
|
34
|
+
4 rows of a metric logged every 10 steps therefore finds nothing — take every
|
|
35
|
+
eval point and slice:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
et.history(-1, metrics=["eval/acc"], dropna=True)[-4:] # the last 4 evals
|
|
39
|
+
```
|
|
40
|
+
|
|
33
41
|
## Output types
|
|
34
42
|
|
|
35
43
|
```python
|
|
@@ -57,6 +57,39 @@ from expr_tracker.history import list_streams
|
|
|
57
57
|
list_streams("runs/llm/sft-1") # [None, "data"]
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
```bash
|
|
61
|
+
et history runs/llm/sft-1 --stream data
|
|
62
|
+
et trace runs/llm/sft-1 # every stream, one lane each
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## A worked example
|
|
66
|
+
|
|
67
|
+
`examples/multiprocess_pipeline.py` runs four data producers and four trainers as
|
|
68
|
+
eight processes sharing one run, with a queue that lets a producer run at most
|
|
69
|
+
`--staleness` batches ahead of the trainers. Each worker writes its own stream,
|
|
70
|
+
so the exported trace gives each one a lane and the blocking spans show which
|
|
71
|
+
side is the bottleneck:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# producers faster than trainers: they stall on a full queue
|
|
75
|
+
uv run python examples/multiprocess_pipeline.py --produce-ms 10 --train-ms 40
|
|
76
|
+
|
|
77
|
+
# trainers faster than producers: they starve waiting for batches
|
|
78
|
+
uv run python examples/multiprocess_pipeline.py --produce-ms 40 --train-ms 10
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
producers 0.38s in produce
|
|
83
|
+
read 0.15s 37.9%
|
|
84
|
+
enqueue 0.13s 35.0% <- backpressure
|
|
85
|
+
decode 0.09s 24.5%
|
|
86
|
+
|
|
87
|
+
trainers 1.05s in step
|
|
88
|
+
backward 0.59s 55.9%
|
|
89
|
+
forward 0.40s 38.1%
|
|
90
|
+
wait_for_batch 0.05s 4.9% <- starvation
|
|
91
|
+
```
|
|
92
|
+
|
|
60
93
|
## Alerts
|
|
61
94
|
|
|
62
95
|
Each process alerts on what it can see, which is its own stream. That is usually
|
|
@@ -36,12 +36,13 @@ or an unserialisable value degrades with a warning; none of them can stop traini
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
uv add expr_tracker # local-first, three small dependencies
|
|
39
|
-
uv add "expr_tracker[all]" # + wandb, trackio,
|
|
39
|
+
uv add "expr_tracker[all]" # + wandb, trackio, pandas, polars, gpu
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
## Next
|
|
43
43
|
|
|
44
44
|
- [Getting started](getting-started.md) — a complete run, end to end.
|
|
45
|
+
- [Examples](examples.md) — six runnable programs, all offline.
|
|
45
46
|
- [Logging metrics](guide/logging.md) — commit semantics and the step model.
|
|
46
47
|
- [Alerts](guide/alerts.md) — rules, channels and delivery policy.
|
|
47
48
|
- [Design](design.md) — the data model and the invariants behind it.
|
|
@@ -26,6 +26,8 @@ et.init(
|
|
|
26
26
|
stream: str | None = None,
|
|
27
27
|
alert=None,
|
|
28
28
|
alert_rules: Sequence = (),
|
|
29
|
+
alert_on_rank: int | None = 0,
|
|
30
|
+
backend_on_rank: int | None = 0,
|
|
29
31
|
**history_options,
|
|
30
32
|
) -> Run
|
|
31
33
|
```
|
|
@@ -34,6 +36,14 @@ Starts a run and publishes it as the process-wide current run. `name` defaults t
|
|
|
34
36
|
timestamp. `dir` defaults to `./tracker/jsonl`. Extra keyword arguments are
|
|
35
37
|
[history options](configuration.md#history-options).
|
|
36
38
|
|
|
39
|
+
`resume` is forwarded to wandb and trackio. The local history always continues an
|
|
40
|
+
existing run directory, so re-running with the same project and name picks up the
|
|
41
|
+
step cursor whatever `resume` says; use a new name to start clean.
|
|
42
|
+
|
|
43
|
+
`alert_on_rank` and `backend_on_rank` pick which rank alerts and which opens the
|
|
44
|
+
remote backend run; `None` means every rank. See
|
|
45
|
+
[Distributed runs](../guide/distributed.md).
|
|
46
|
+
|
|
37
47
|
Calling `init()` twice without `finish()` raises.
|
|
38
48
|
|
|
39
49
|
### `finish`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Every example runs offline: no account, no network, no GPU required. They write
|
|
4
|
+
to `runs/` by default, which is gitignored — pass `--dir` to put them elsewhere.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
uv run python examples/quickstart.py
|
|
8
|
+
uv run python examples/<name>.py --help # every example takes arguments
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## [`quickstart.py`](https://github.com/HSPK/expr_tracker/blob/main/examples/quickstart.py)
|
|
12
|
+
|
|
13
|
+
The sixty-second tour. Logs a training run, merges a sparse eval metric into the
|
|
14
|
+
training step with `commit=False`, queries the history while the run is still
|
|
15
|
+
open, and reads it back afterwards from the directory.
|
|
16
|
+
|
|
17
|
+
## [`alert_rules.py`](https://github.com/HSPK/expr_tracker/blob/main/examples/alert_rules.py)
|
|
18
|
+
|
|
19
|
+
Four rules against four faults: a loss spike caught by z-score, a non-finite
|
|
20
|
+
loss, a curve that goes flat, and an accuracy regression that has to persist for
|
|
21
|
+
three steps before it counts.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv run python examples/alert_rules.py --fault spike # or nan, stall, none
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`--fault none` sends nothing. That is the interesting case: warm-up, missing
|
|
28
|
+
data and NaN all evaluate to UNKNOWN rather than False, so rules cannot cry wolf
|
|
29
|
+
before they have the evidence.
|
|
30
|
+
|
|
31
|
+
## [`profile_step.py`](https://github.com/HSPK/expr_tracker/blob/main/examples/profile_step.py)
|
|
32
|
+
|
|
33
|
+
Where a training step actually goes. Nested spans become metrics on the step's
|
|
34
|
+
row, so timings query and alert like any other metric; a plugin attaches CPU
|
|
35
|
+
cost; the tree exports to a Chrome Trace for Perfetto.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run python examples/profile_step.py --print-spans
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## [`early_stopping.py`](https://github.com/HSPK/expr_tracker/blob/main/examples/early_stopping.py)
|
|
42
|
+
|
|
43
|
+
The loop reading its own history to decide what to do next: decay the learning
|
|
44
|
+
rate when the eval metric plateaus, and stop once decaying no longer pays. This
|
|
45
|
+
is what a local, queryable history buys you over shipping metrics out.
|
|
46
|
+
|
|
47
|
+
## [`checkpoints.py`](https://github.com/HSPK/expr_tracker/blob/main/examples/checkpoints.py)
|
|
48
|
+
|
|
49
|
+
Checkpoints as artifacts — versioned, deduplicated by content, aliased `best`,
|
|
50
|
+
and fetched by a later run that knows nothing about which run wrote them.
|
|
51
|
+
|
|
52
|
+
## [`multiprocess_pipeline.py`](https://github.com/HSPK/expr_tracker/blob/main/examples/multiprocess_pipeline.py)
|
|
53
|
+
|
|
54
|
+
Four data producers and four trainers as eight processes sharing one run, with a
|
|
55
|
+
queue that lets a producer run at most `--staleness` batches ahead. Each worker
|
|
56
|
+
writes its own stream and gets its own lane in the trace, and the blocking spans
|
|
57
|
+
show which side is the bottleneck.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# producers faster than trainers: they stall on a full queue
|
|
61
|
+
uv run python examples/multiprocess_pipeline.py --produce-ms 10 --train-ms 40
|
|
62
|
+
|
|
63
|
+
# trainers faster than producers: they starve waiting for batches
|
|
64
|
+
uv run python examples/multiprocess_pipeline.py --produce-ms 40 --train-ms 10
|
|
65
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Alert rules that catch the four ways a run goes wrong.
|
|
2
|
+
|
|
3
|
+
Rules are expressions over a rolling window of your metrics, evaluated once per
|
|
4
|
+
committed step. This sends to a local handler so it runs offline; swap the
|
|
5
|
+
channel for lark/slack/email and nothing else changes.
|
|
6
|
+
|
|
7
|
+
uv run python examples/alert_rules.py
|
|
8
|
+
uv run python examples/alert_rules.py --fault stall
|
|
9
|
+
|
|
10
|
+
Faults: spike, nan, stall, none.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import math
|
|
17
|
+
import random
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
import expr_tracker as et
|
|
21
|
+
|
|
22
|
+
RULES = [
|
|
23
|
+
# Never fires during warm-up: too few points evaluates to UNKNOWN, not False
|
|
24
|
+
"zscore(train/loss[30]) > 4 => error: loss spike {train/loss:.4f} @ step {step}",
|
|
25
|
+
"isnan(train/loss) => critical: loss went non-finite",
|
|
26
|
+
"stalled(train/loss[20]) => warning: loss flat for 20 steps",
|
|
27
|
+
# Must hold for 3 consecutive steps, so one noisy eval does not page anyone
|
|
28
|
+
{
|
|
29
|
+
"name": "accuracy_regression",
|
|
30
|
+
"condition": "eval/accuracy < 0.5",
|
|
31
|
+
"level": "warning",
|
|
32
|
+
"for_steps": 3,
|
|
33
|
+
"notify_recovery": True,
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def channel(sink):
|
|
39
|
+
"""A channel that appends to a list. Synchronous, so output stays ordered."""
|
|
40
|
+
return {
|
|
41
|
+
"channels": [
|
|
42
|
+
{
|
|
43
|
+
"type": "callable",
|
|
44
|
+
"name": "local",
|
|
45
|
+
"options": {"handler": sink.append},
|
|
46
|
+
"policy": {"async_send": False, "dedup_window": 0},
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def losses(args):
|
|
53
|
+
"""A loss curve with the requested fault injected into it."""
|
|
54
|
+
rng = random.Random(args.seed)
|
|
55
|
+
for step in range(args.steps):
|
|
56
|
+
loss = math.exp(-step / 60) + rng.uniform(0, 0.02)
|
|
57
|
+
if args.fault == "spike" and step == args.at:
|
|
58
|
+
loss *= 12 # a sudden jump the z-score will notice
|
|
59
|
+
elif args.fault == "nan" and step >= args.at:
|
|
60
|
+
loss = float("nan")
|
|
61
|
+
elif args.fault == "stall" and step >= args.at:
|
|
62
|
+
loss = 0.5 # exactly flat, which stalled() is looking for
|
|
63
|
+
yield step, loss
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv=None) -> list:
|
|
67
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--fault", default="spike", choices=["spike", "nan", "stall", "none"]
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument("--steps", type=int, default=80)
|
|
72
|
+
parser.add_argument("--at", type=int, default=50, help="step to inject the fault")
|
|
73
|
+
parser.add_argument("--seed", type=int, default=0)
|
|
74
|
+
parser.add_argument("--dir", default="runs")
|
|
75
|
+
parser.add_argument("--name", default=None)
|
|
76
|
+
args = parser.parse_args(argv)
|
|
77
|
+
# A fresh run each time: resuming would leave the previous run's losses in
|
|
78
|
+
# the rolling window, and the jump back to a high loss reads as a spike
|
|
79
|
+
args.name = args.name or f"{args.fault}-{time.strftime('%H%M%S')}"
|
|
80
|
+
|
|
81
|
+
fired: list = []
|
|
82
|
+
et.init(
|
|
83
|
+
project="alerts",
|
|
84
|
+
name=args.name,
|
|
85
|
+
dir=args.dir,
|
|
86
|
+
backends=[],
|
|
87
|
+
alert=channel(fired),
|
|
88
|
+
alert_rules=RULES,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
for step, loss in losses(args):
|
|
92
|
+
accuracy = 0.4 if args.fault == "stall" and step > args.at else 0.9
|
|
93
|
+
et.log({"eval/accuracy": accuracy}, commit=False)
|
|
94
|
+
et.log({"train/loss": loss})
|
|
95
|
+
|
|
96
|
+
print(f"fault: {args.fault}, {args.steps} steps")
|
|
97
|
+
print(f"\n{len(fired)} message(s):")
|
|
98
|
+
for message in fired:
|
|
99
|
+
print(f" [{message.level.value}] {message.title}")
|
|
100
|
+
print(f" {message.text.splitlines()[0]}")
|
|
101
|
+
|
|
102
|
+
state = et.info()["alerts"]["rules"]
|
|
103
|
+
print("\nrule state:")
|
|
104
|
+
for rule in et.list_alert_rules():
|
|
105
|
+
# An unnamed rule is known by its condition, which is what you read
|
|
106
|
+
label = rule.condition if rule.auto_named else rule.name
|
|
107
|
+
counts = state[rule.name]
|
|
108
|
+
print(f" {label:<34} fires={counts['fires']} firing={counts['firing']}")
|
|
109
|
+
|
|
110
|
+
et.finish()
|
|
111
|
+
return fired
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
main()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Checkpoints as artifacts: versioned, deduplicated, and found again later.
|
|
2
|
+
|
|
3
|
+
Artifacts are stored per project and shared by its runs, so a later run can ask
|
|
4
|
+
for ``model:best`` without knowing which run produced it.
|
|
5
|
+
|
|
6
|
+
uv run python examples/checkpoints.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import expr_tracker as et
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def write_checkpoint(directory: Path, step: int, accuracy: float) -> Path:
|
|
19
|
+
"""Stand-in for torch.save."""
|
|
20
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
path = directory / "model.pt"
|
|
22
|
+
path.write_text(f"weights@{step}:{accuracy:.3f}", encoding="utf-8")
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def train(args, workdir: Path) -> str:
|
|
27
|
+
run = et.init(
|
|
28
|
+
project="checkpoints",
|
|
29
|
+
name=args.name,
|
|
30
|
+
dir=args.dir,
|
|
31
|
+
backends=[],
|
|
32
|
+
config={"steps": args.steps},
|
|
33
|
+
)
|
|
34
|
+
best = -1.0
|
|
35
|
+
|
|
36
|
+
for step in range(0, args.steps, args.every):
|
|
37
|
+
accuracy = 0.7 + 0.1 * (step / args.steps)
|
|
38
|
+
et.log({"eval/accuracy": accuracy}, commit=False)
|
|
39
|
+
et.log({"train/loss": 1.0 / (step + 1)})
|
|
40
|
+
|
|
41
|
+
path = write_checkpoint(workdir, step, accuracy)
|
|
42
|
+
aliases = ["best"] if accuracy > best else []
|
|
43
|
+
artifact = et.log_artifact(path, name="model", type="model", aliases=aliases)
|
|
44
|
+
if accuracy > best:
|
|
45
|
+
best = accuracy
|
|
46
|
+
et.summary()["best_accuracy"] = accuracy
|
|
47
|
+
print(f"step {step:>3}: acc {accuracy:.3f} -> model:v{artifact.version}")
|
|
48
|
+
|
|
49
|
+
# The same bytes again: deduplicated, so no new version is created
|
|
50
|
+
repeat = et.log_artifact(workdir / "model.pt", name="model", type="model")
|
|
51
|
+
print(f"logging the same file again -> v{repeat.version} (deduplicated)")
|
|
52
|
+
|
|
53
|
+
et.finish()
|
|
54
|
+
return run.name
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def restore(args, into: Path) -> None:
|
|
58
|
+
"""A different run, later, asking for the best checkpoint."""
|
|
59
|
+
et.init(project="checkpoints", name=f"{args.name}-eval", dir=args.dir, backends=[])
|
|
60
|
+
artifact = et.use_artifact("model:best")
|
|
61
|
+
path = Path(artifact.download(str(into)))
|
|
62
|
+
payload = (path / "model.pt").read_text(encoding="utf-8")
|
|
63
|
+
print(f"\nrestored model:best (v{artifact.version}) -> {payload}")
|
|
64
|
+
print(f"downloaded to {path}")
|
|
65
|
+
et.finish()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main(argv=None) -> None:
|
|
69
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
70
|
+
parser.add_argument("--steps", type=int, default=40)
|
|
71
|
+
parser.add_argument("--every", type=int, default=10, help="checkpoint interval")
|
|
72
|
+
parser.add_argument("--dir", default="runs")
|
|
73
|
+
parser.add_argument("--name", default=None)
|
|
74
|
+
args = parser.parse_args(argv)
|
|
75
|
+
args.name = args.name or time.strftime("ckpt-%H%M%S")
|
|
76
|
+
|
|
77
|
+
workdir = Path(args.dir) / "scratch" / args.name
|
|
78
|
+
train(args, workdir)
|
|
79
|
+
restore(args, workdir / "restored")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
main()
|