expr-tracker 0.2.1__tar.gz → 0.2.3__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.1 → expr_tracker-0.2.3}/.gitignore +5 -0
- expr_tracker-0.2.3/LICENSE +21 -0
- expr_tracker-0.2.1/README.md → expr_tracker-0.2.3/PKG-INFO +61 -0
- expr_tracker-0.2.1/PKG-INFO → expr_tracker-0.2.3/README.md +14 -27
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/design.md +42 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/alerts.md +63 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/cli.md +19 -0
- expr_tracker-0.2.3/docs/guide/spans.md +169 -0
- expr_tracker-0.2.3/docs/guide/streams.md +113 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/reference/api.md +26 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/reference/configuration.md +4 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/mkdocs.yml +2 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/pyproject.toml +34 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/__init__.py +4 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/backends/__init__.py +5 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/backends/base.py +70 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/engine.py +6 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/models.py +3 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/cli.py +24 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/__init__.py +13 -2
- expr_tracker-0.2.3/src/expr_tracker/history/naming.py +74 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/reader.py +40 -10
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/store.py +96 -28
- expr_tracker-0.2.3/src/expr_tracker/py.typed +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/run.py +27 -6
- expr_tracker-0.2.3/src/expr_tracker/spans.py +236 -0
- expr_tracker-0.2.3/src/expr_tracker/trace.py +189 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/tracker.py +27 -5
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_alert_backends.py +179 -1
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_distributed.py +4 -4
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_rule_lifecycle.py +31 -0
- expr_tracker-0.2.3/tests/test_spans.py +620 -0
- expr_tracker-0.2.3/tests/test_streams.py +518 -0
- expr_tracker-0.2.3/tests/test_trace.py +421 -0
- expr_tracker-0.2.1/outputs/hello/test/metrics.jsonl +0 -5
- expr_tracker-0.2.1/outputs/hello/test/metrics.meta.json +0 -1
- expr_tracker-0.2.1/outputs/hello/test/summary.json +0 -6
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/.github/workflows/docs.yaml +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/.github/workflows/release.yaml +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/architecture.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/getting-started.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/artifacts.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/backends.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/distributed.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/history.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/guide/logging.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/index.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/docs/reference/expressions.md +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/_compat.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/__init__.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/dispatch.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/__init__.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/eval.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/functions.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/lexer.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/nodes.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/parser.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/alerts/expr/rule.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/artifacts.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/encoders.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/codec.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/frame.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/series.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/history/writer.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/summary.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/src/expr_tracker/types.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/conftest.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_alert_delivery.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_alert_dispatch.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_alert_engine.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_alert_models.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_alert_routing.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_artifacts.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_benchmark.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_cache.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_cli.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_correctness.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_e2e.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_expr_builder.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_expr_eval.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_expr_functions.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_expr_parser.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_expr_properties.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_failure_modes.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_features.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_frame_codec_summary.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_history.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_hot_paths.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_integration.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_lark_live.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_perf.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_public_surfaces.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_review_regressions.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_run_backends.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_scenarios.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_step_commit.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_stress.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_trackio.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_value_encoding.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_wandb.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_writer_buffer.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/tests/test_writer_durability.py +0 -0
- {expr_tracker-0.2.1 → expr_tracker-0.2.3}/uv.lock +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hangxing Wei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,5 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: expr_tracker
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Local-first experiment tracking with queryable history and expression-based alerts on your training metrics
|
|
5
|
+
Project-URL: Homepage, https://hspk.github.io/expr_tracker/
|
|
6
|
+
Project-URL: Documentation, https://hspk.github.io/expr_tracker/
|
|
7
|
+
Project-URL: Repository, https://github.com/HSPK/expr_tracker
|
|
8
|
+
Project-URL: Issues, https://github.com/HSPK/expr_tracker/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/HSPK/expr_tracker/releases
|
|
10
|
+
Author-email: HSPK <whxway@whu.edu.cn>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: alerting,deep-learning,experiment-tracking,machine-learning,mlops,monitoring,training,wandb
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: System :: Monitoring
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: click>=8.1.0
|
|
28
|
+
Requires-Dist: loguru>=0.7.3
|
|
29
|
+
Requires-Dist: pydantic>=2.0
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: pandas>=1.5; extra == 'all'
|
|
32
|
+
Requires-Dist: polars>=0.20; extra == 'all'
|
|
33
|
+
Requires-Dist: slark>=0.1.28; extra == 'all'
|
|
34
|
+
Requires-Dist: trackio>=0.4.0; extra == 'all'
|
|
35
|
+
Requires-Dist: wandb>=0.21.0; extra == 'all'
|
|
36
|
+
Provides-Extra: lark
|
|
37
|
+
Requires-Dist: slark>=0.1.28; extra == 'lark'
|
|
38
|
+
Provides-Extra: pandas
|
|
39
|
+
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
40
|
+
Provides-Extra: polars
|
|
41
|
+
Requires-Dist: polars>=0.20; extra == 'polars'
|
|
42
|
+
Provides-Extra: trackio
|
|
43
|
+
Requires-Dist: trackio>=0.4.0; extra == 'trackio'
|
|
44
|
+
Provides-Extra: wandb
|
|
45
|
+
Requires-Dist: wandb>=0.21.0; extra == 'wandb'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
1
48
|
# Experiment Tracker
|
|
2
49
|
|
|
50
|
+
[](https://pypi.org/project/expr-tracker/)
|
|
51
|
+
[](https://pypi.org/project/expr-tracker/)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
[](https://hspk.github.io/expr_tracker/)
|
|
54
|
+
|
|
3
55
|
A local-first experiment tracker. Metrics land in a JSONL file you own, stay
|
|
4
56
|
queryable while the run is live, and can trigger alerts from an expression language.
|
|
5
57
|
`wandb` and `trackio` are optional mirrors, not requirements.
|
|
@@ -54,6 +106,8 @@ install command; it never crashes a run.
|
|
|
54
106
|
| [Alerts](https://hspk.github.io/expr_tracker/guide/alerts/) | An expression DSL with rolling windows, three-valued logic (no false alarms during warm-up), a rule state machine, and a watchdog that catches a hung run. |
|
|
55
107
|
| [Channels](https://hspk.github.io/expr_tracker/guide/alerts/#channels) | Lark, Slack, DingTalk, WeCom, generic webhook, email — with rate limiting, dedup, retries and per-channel routing. |
|
|
56
108
|
| [Artifacts](https://hspk.github.io/expr_tracker/guide/artifacts/) | Versioned file sets, deduplicated by content, shared across a project's runs, with lineage. |
|
|
109
|
+
| [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. |
|
|
110
|
+
| [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. |
|
|
57
111
|
| [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. |
|
|
58
112
|
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et rules explain`, `et rules test`, `et alert`. |
|
|
59
113
|
|
|
@@ -96,6 +150,9 @@ uv run ruff format src tests
|
|
|
96
150
|
| `test_hot_paths.py` | contracts and defaults of `et.log` / `et.history` / summary / alerts |
|
|
97
151
|
| `test_value_encoding.py` | numpy, pydantic, datetime, Path, Enum round trips; output types; query bounds |
|
|
98
152
|
| `test_expr_properties.py` | DSL properties: render round-trip stability, precedence, the whole `M` builder |
|
|
153
|
+
| `test_trace.py` | Chrome Trace export: lane layout, stream and step selection, the CLI |
|
|
154
|
+
| `test_spans.py` | nesting, aggregation, decorator and async forms, errors, thread and task isolation |
|
|
155
|
+
| `test_streams.py` | stream naming and validation, isolation, resolution order, backend grouping, two-process runs |
|
|
99
156
|
| `test_distributed.py` | rank shards, `alert_on_rank`, real multi-process runs |
|
|
100
157
|
| `test_wandb.py` | real wandb in offline mode: parameter mapping, step alignment, artifacts |
|
|
101
158
|
| `test_trackio.py` | trackio contract, resume mapping, real end-to-end |
|
|
@@ -114,3 +171,7 @@ Published to GitHub Pages by `.github/workflows/docs.yaml` on every push to `mai
|
|
|
114
171
|
Internals: [`docs/design.md`](docs/design.md) (data model and key invariants) and
|
|
115
172
|
[`docs/architecture.md`](docs/architecture.md) (module map, read/write paths,
|
|
116
173
|
concurrency model).
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
[MIT](LICENSE)
|
|
@@ -1,32 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: expr_tracker
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: Add your description here
|
|
5
|
-
Author-email: HSPK <whxway@whu.edu.cn>
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Requires-Dist: click>=8.1.0
|
|
8
|
-
Requires-Dist: loguru>=0.7.3
|
|
9
|
-
Requires-Dist: pydantic>=2.0
|
|
10
|
-
Provides-Extra: all
|
|
11
|
-
Requires-Dist: pandas>=1.5; extra == 'all'
|
|
12
|
-
Requires-Dist: polars>=0.20; extra == 'all'
|
|
13
|
-
Requires-Dist: slark>=0.1.28; extra == 'all'
|
|
14
|
-
Requires-Dist: trackio>=0.4.0; extra == 'all'
|
|
15
|
-
Requires-Dist: wandb>=0.21.0; extra == 'all'
|
|
16
|
-
Provides-Extra: lark
|
|
17
|
-
Requires-Dist: slark>=0.1.28; extra == 'lark'
|
|
18
|
-
Provides-Extra: pandas
|
|
19
|
-
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
20
|
-
Provides-Extra: polars
|
|
21
|
-
Requires-Dist: polars>=0.20; extra == 'polars'
|
|
22
|
-
Provides-Extra: trackio
|
|
23
|
-
Requires-Dist: trackio>=0.4.0; extra == 'trackio'
|
|
24
|
-
Provides-Extra: wandb
|
|
25
|
-
Requires-Dist: wandb>=0.21.0; extra == 'wandb'
|
|
26
|
-
Description-Content-Type: text/markdown
|
|
27
|
-
|
|
28
1
|
# Experiment Tracker
|
|
29
2
|
|
|
3
|
+
[](https://pypi.org/project/expr-tracker/)
|
|
4
|
+
[](https://pypi.org/project/expr-tracker/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://hspk.github.io/expr_tracker/)
|
|
7
|
+
|
|
30
8
|
A local-first experiment tracker. Metrics land in a JSONL file you own, stay
|
|
31
9
|
queryable while the run is live, and can trigger alerts from an expression language.
|
|
32
10
|
`wandb` and `trackio` are optional mirrors, not requirements.
|
|
@@ -81,6 +59,8 @@ install command; it never crashes a run.
|
|
|
81
59
|
| [Alerts](https://hspk.github.io/expr_tracker/guide/alerts/) | An expression DSL with rolling windows, three-valued logic (no false alarms during warm-up), a rule state machine, and a watchdog that catches a hung run. |
|
|
82
60
|
| [Channels](https://hspk.github.io/expr_tracker/guide/alerts/#channels) | Lark, Slack, DingTalk, WeCom, generic webhook, email — with rate limiting, dedup, retries and per-channel routing. |
|
|
83
61
|
| [Artifacts](https://hspk.github.io/expr_tracker/guide/artifacts/) | Versioned file sets, deduplicated by content, shared across a project's runs, with lineage. |
|
|
62
|
+
| [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. |
|
|
63
|
+
| [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. |
|
|
84
64
|
| [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. |
|
|
85
65
|
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et rules explain`, `et rules test`, `et alert`. |
|
|
86
66
|
|
|
@@ -123,6 +103,9 @@ uv run ruff format src tests
|
|
|
123
103
|
| `test_hot_paths.py` | contracts and defaults of `et.log` / `et.history` / summary / alerts |
|
|
124
104
|
| `test_value_encoding.py` | numpy, pydantic, datetime, Path, Enum round trips; output types; query bounds |
|
|
125
105
|
| `test_expr_properties.py` | DSL properties: render round-trip stability, precedence, the whole `M` builder |
|
|
106
|
+
| `test_trace.py` | Chrome Trace export: lane layout, stream and step selection, the CLI |
|
|
107
|
+
| `test_spans.py` | nesting, aggregation, decorator and async forms, errors, thread and task isolation |
|
|
108
|
+
| `test_streams.py` | stream naming and validation, isolation, resolution order, backend grouping, two-process runs |
|
|
126
109
|
| `test_distributed.py` | rank shards, `alert_on_rank`, real multi-process runs |
|
|
127
110
|
| `test_wandb.py` | real wandb in offline mode: parameter mapping, step alignment, artifacts |
|
|
128
111
|
| `test_trackio.py` | trackio contract, resume mapping, real end-to-end |
|
|
@@ -141,3 +124,7 @@ Published to GitHub Pages by `.github/workflows/docs.yaml` on every push to `mai
|
|
|
141
124
|
Internals: [`docs/design.md`](docs/design.md) (data model and key invariants) and
|
|
142
125
|
[`docs/architecture.md`](docs/architecture.md) (module map, read/write paths,
|
|
143
126
|
concurrency model).
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
[MIT](LICENSE)
|
|
@@ -75,7 +75,48 @@ last line. Patch lines and `step_policy="allow"` both let a file end on a lower
|
|
|
75
75
|
and continuing from that would reuse an existing one. `max_step` is maintained
|
|
76
76
|
incrementally and persisted in the sidecar.
|
|
77
77
|
|
|
78
|
-
### A.4
|
|
78
|
+
### A.4 Streams
|
|
79
|
+
|
|
80
|
+
Independent producers — a training loop and a data worker — have unrelated step
|
|
81
|
+
semantics, so each gets its own file, cursor and resume state under one run
|
|
82
|
+
directory: `metrics[.stream][.rankN].jsonl`, plus per-stream `summary` and
|
|
83
|
+
`config` sidecars so concurrent processes cannot clobber each other.
|
|
84
|
+
|
|
85
|
+
The store is instantiated once per stream rather than made multi-tenant, so every
|
|
86
|
+
invariant below holds unchanged within a stream; only the filename differs.
|
|
87
|
+
|
|
88
|
+
`resolve_run_path()` matches names exactly rather than taking the first glob hit:
|
|
89
|
+
`metrics.data.jsonl` sorts before `metrics.jsonl`, so sort order would silently
|
|
90
|
+
return a stream in place of the default producer.
|
|
91
|
+
|
|
92
|
+
A stream is forwarded to a backend as its own run, grouped under the run name.
|
|
93
|
+
Neither wandb nor trackio can merge two step axes into one run; wandb's shared
|
|
94
|
+
mode can, but requires a live server and has no trackio equivalent.
|
|
95
|
+
|
|
96
|
+
### A.5 Spans
|
|
97
|
+
|
|
98
|
+
`et.span` times a region and its children. A closed span takes the same ingress as
|
|
99
|
+
`log()` — `HistoryStore.ingest()` — so the step policy, the open-row merge and the
|
|
100
|
+
commit rules exist in one place regardless of which API produced the metrics.
|
|
101
|
+
|
|
102
|
+
Two differences from a log call:
|
|
103
|
+
|
|
104
|
+
- the merge **accumulates** rather than replaces, because the same span can run
|
|
105
|
+
many times in one step;
|
|
106
|
+
- the metrics are built here from floats and ints, so they skip `RecordCodec`.
|
|
107
|
+
That encoder costs 5 µs, which a per-sub-step call cannot afford.
|
|
108
|
+
|
|
109
|
+
A span never commits a step: durations ride along with whatever `log()` commits,
|
|
110
|
+
so timing a region adds no row. The tree, with timestamps and attributes, goes to
|
|
111
|
+
`spans[.stream][.rankN].jsonl` through a second `JsonlWriter`, enqueued rather
|
|
112
|
+
than appended so a span does not pay for a flush decision of its own.
|
|
113
|
+
|
|
114
|
+
The nesting stack is a `ContextVar`, not a `threading.local`: a new thread starts
|
|
115
|
+
from the default and each asyncio task gets its own copy, which is exactly the
|
|
116
|
+
scoping spans need. (The run singleton went the other way, from `ContextVar` to a
|
|
117
|
+
locked global, because there the goal was for worker threads to *share* it.)
|
|
118
|
+
|
|
119
|
+
### A.6 Multiple processes
|
|
79
120
|
|
|
80
121
|
The supported model is **rank 0 tracks**; there is no cross-rank merging.
|
|
81
122
|
|
|
@@ -93,6 +93,69 @@ Built-in types: `lark`, `slack`, `dingtalk`, `wecom`, `webhook` (a generic JSON
|
|
|
93
93
|
template), `email`, `callable`. All but `lark` use only the standard library. Add
|
|
94
94
|
your own with `register_backend()`.
|
|
95
95
|
|
|
96
|
+
### Email
|
|
97
|
+
|
|
98
|
+
Email needs an SMTP server to send *from*, even when the recipient is Gmail.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
et.init(
|
|
102
|
+
project="demo",
|
|
103
|
+
alert={
|
|
104
|
+
"channels": [
|
|
105
|
+
{
|
|
106
|
+
"type": "email",
|
|
107
|
+
"name": "inbox",
|
|
108
|
+
"options": {
|
|
109
|
+
"host": "smtp.gmail.com",
|
|
110
|
+
"port": 587,
|
|
111
|
+
"tls": True,
|
|
112
|
+
"user": "you@gmail.com",
|
|
113
|
+
"password": os.environ["SMTP_PASSWORD"],
|
|
114
|
+
"sender": "you@gmail.com",
|
|
115
|
+
"to": ["you@gmail.com", "teammate@example.com"],
|
|
116
|
+
},
|
|
117
|
+
"min_level": "error",
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
alert_rules=["isnan(loss) => critical: loss diverged"],
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Option | Default | Meaning |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `host` | — | SMTP server, **required** |
|
|
128
|
+
| `to` | — | one address or a list, **required** |
|
|
129
|
+
| `port` | 465 with `ssl`, else 25 | |
|
|
130
|
+
| `tls` | `false` | STARTTLS on a plain connection (port 587) |
|
|
131
|
+
| `ssl` | `false` | implicit TLS from the start (port 465) |
|
|
132
|
+
| `user` / `password` | — | omit both for an unauthenticated relay |
|
|
133
|
+
| `sender` | `user`, else `expr-tracker` | the `From` address |
|
|
134
|
+
| `html` | `true` | send the HTML part as well as the text |
|
|
135
|
+
|
|
136
|
+
Use `tls` **or** `ssl`, not both: `tls` upgrades a plain connection, `ssl` starts
|
|
137
|
+
encrypted.
|
|
138
|
+
|
|
139
|
+
!!! warning "Keep the password out of your code"
|
|
140
|
+
Read it from the environment, as above. Gmail additionally rejects account
|
|
141
|
+
passwords for SMTP — turn on 2-step verification and create an
|
|
142
|
+
[app password](https://myaccount.google.com/apppasswords), which you can
|
|
143
|
+
revoke independently of your account.
|
|
144
|
+
|
|
145
|
+
Mail is sent as `multipart/alternative`: a severity-coloured HTML card with the
|
|
146
|
+
fields as a table, plus the plain text as a fallback for clients that will not
|
|
147
|
+
render HTML. Set `html: false` for text only.
|
|
148
|
+
|
|
149
|
+
Common servers:
|
|
150
|
+
|
|
151
|
+
| Provider | host | port | setting |
|
|
152
|
+
| --- | --- | --- | --- |
|
|
153
|
+
| Gmail | `smtp.gmail.com` | 587 | `tls: true` (app password required) |
|
|
154
|
+
| Outlook / Office 365 | `smtp.office365.com` | 587 | `tls: true` |
|
|
155
|
+
| QQ / 163 | `smtp.qq.com`, `smtp.163.com` | 465 | `ssl: true` (authorisation code) |
|
|
156
|
+
| SendGrid | `smtp.sendgrid.net` | 587 | `tls: true`, user `apikey` |
|
|
157
|
+
| Internal relay | your host | 25 | often no `user`/`password` |
|
|
158
|
+
|
|
96
159
|
### Routing
|
|
97
160
|
|
|
98
161
|
Each channel filters independently:
|
|
@@ -22,6 +22,25 @@ et history tracker/jsonl/demo/run-1 -n -1 --format csv > run.csv
|
|
|
22
22
|
|
|
23
23
|
The argument is a run directory or a `metrics.jsonl` file.
|
|
24
24
|
|
|
25
|
+
## `et trace`
|
|
26
|
+
|
|
27
|
+
Export recorded [spans](spans.md) as a Chrome Trace, for
|
|
28
|
+
[Perfetto](https://ui.perfetto.dev) or `chrome://tracing`.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
et trace runs/llm/sft-1 # every stream -> trace.json
|
|
32
|
+
et trace runs/llm/sft-1 -o pipeline.json
|
|
33
|
+
et trace runs/llm/sft-1 --stream data # one stream
|
|
34
|
+
et trace runs/llm/sft-1 --stream default,data # several
|
|
35
|
+
et trace runs/llm/sft-1 --step-range 100:200 # a slice of the run
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Option | Default | Meaning |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `-o`, `--output` | `trace.json` | where to write |
|
|
41
|
+
| `--stream` | every stream | comma separated; `default` is the unnamed one |
|
|
42
|
+
| `--step-range` | all | `start:end`, end exclusive |
|
|
43
|
+
|
|
25
44
|
## `et rules explain`
|
|
26
45
|
|
|
27
46
|
Show how an expression parses, and what it references. Useful when precedence is in
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Spans
|
|
2
|
+
|
|
3
|
+
A step is rarely one thing. `et.span` times the parts, and their parts, and turns
|
|
4
|
+
each duration into an ordinary metric — so `history()`, alert rules and plots all
|
|
5
|
+
work on it with no extra machinery.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
for batch in loader:
|
|
9
|
+
with et.span("forward"):
|
|
10
|
+
with et.span("attention"):
|
|
11
|
+
attn = attention(batch)
|
|
12
|
+
with et.span("mlp"):
|
|
13
|
+
out = mlp(attn)
|
|
14
|
+
with et.span("backward"):
|
|
15
|
+
loss.backward()
|
|
16
|
+
et.log({"loss": loss.item()})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The step's row then carries:
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
{"_step": 42, "_time": ...,
|
|
23
|
+
"forward/duration_ms": 31.2, "forward/count": 1,
|
|
24
|
+
"forward/attention/duration_ms": 18.4, "forward/attention/count": 1,
|
|
25
|
+
"forward/mlp/duration_ms": 9.1, "forward/mlp/count": 1,
|
|
26
|
+
"backward/duration_ms": 44.7, "backward/count": 1,
|
|
27
|
+
"loss": 0.31}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Nested names join with `/`, so `forward/norm` and `backward/norm` stay distinct,
|
|
31
|
+
and the alert language reads them directly.
|
|
32
|
+
|
|
33
|
+
## Forms
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
with et.span("forward"): ... # context manager
|
|
37
|
+
|
|
38
|
+
async with et.span("fetch"): ... # async
|
|
39
|
+
|
|
40
|
+
@et.span("preprocess") # decorator, sync or async
|
|
41
|
+
def preprocess(batch): ...
|
|
42
|
+
|
|
43
|
+
span = et.start_span("epoch") # manual, for crossing scopes
|
|
44
|
+
...
|
|
45
|
+
span.end()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Repeated spans
|
|
49
|
+
|
|
50
|
+
The same span several times in one step sums, and counts:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
for layer in layers: # 32 layers
|
|
54
|
+
with et.span("layer"):
|
|
55
|
+
x = layer(x)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
{"layer/duration_ms": 412.8, "layer/count": 32}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The total is usually what you want; divide by the count for the mean.
|
|
63
|
+
|
|
64
|
+
## A span never commits a step
|
|
65
|
+
|
|
66
|
+
Durations join whatever `log()` commits, so a span costs no row of its own and
|
|
67
|
+
you can time things before you know what to log:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with et.span("forward"):
|
|
71
|
+
...
|
|
72
|
+
# nothing written yet
|
|
73
|
+
et.log({"loss": loss}) # one row, with the metrics and the durations
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Attributes
|
|
77
|
+
|
|
78
|
+
Attributes describe an individual span. They reach `spans.jsonl`, not the metrics,
|
|
79
|
+
because they are usually not numbers:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
with et.span("load", batch_size=32) as span:
|
|
83
|
+
rows = read()
|
|
84
|
+
span.set(rows=len(rows))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Alerting on durations
|
|
88
|
+
|
|
89
|
+
Because a duration is a metric, the [expression language](../reference/expressions.md)
|
|
90
|
+
already handles it:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
et.init(..., alert_rules=[
|
|
94
|
+
"mean(data/load_ms[50]) > 200 => warning: data loading is slowing down",
|
|
95
|
+
"forward/duration_ms > 3 * mean(forward/duration_ms[100]) => error: slow step",
|
|
96
|
+
])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## The span file
|
|
100
|
+
|
|
101
|
+
The full tree is written to `spans.jsonl` beside the metrics, one line per span:
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
{"_step": 42, "name": "forward/attention", "depth": 1,
|
|
105
|
+
"start": 1754323200.123456, "dur_ms": 18.4, "args": {"batch_size": 32}}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Children appear before their parents, because a parent closes last. It follows
|
|
109
|
+
the [stream](streams.md): a data worker writes `spans.data.jsonl`.
|
|
110
|
+
|
|
111
|
+
Turn it off if you only want the metrics:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
et.init(..., spans=False)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Viewing the timeline
|
|
118
|
+
|
|
119
|
+
`et trace` turns the span file into a Chrome Trace, which
|
|
120
|
+
[Perfetto](https://ui.perfetto.dev) and `chrome://tracing` open directly:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
et trace runs/llm/sft-1 -o trace.json
|
|
124
|
+
et trace runs/llm/sft-1 --stream data --step-range 100:200
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Each stream becomes a process and each thread a track, so a data worker and a
|
|
128
|
+
training loop sit on one timeline and the gap where one waited for the other is
|
|
129
|
+
visible. Spans keep their nesting, their step and their attributes.
|
|
130
|
+
|
|
131
|
+
Exporting a standard format rather than drawing our own view means the result
|
|
132
|
+
can be loaded beside a `torch.profiler` trace, which is usually where the real
|
|
133
|
+
question is: what were the GPUs doing while the loader stalled.
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
An exception is recorded and re-raised — the span never swallows it:
|
|
138
|
+
|
|
139
|
+
```jsonc
|
|
140
|
+
{"_step": 42, "name": "risky", "dur_ms": 3.1, "error": "ValueError"}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A failure inside the recording itself is logged and ignored: measuring something
|
|
144
|
+
must not break it.
|
|
145
|
+
|
|
146
|
+
## Threads and tasks
|
|
147
|
+
|
|
148
|
+
The nesting stack is per thread and per asyncio task, so concurrent work does not
|
|
149
|
+
nest inside unrelated spans:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
async def work(name):
|
|
153
|
+
async with et.span(name): # "a" and "b", never "a/b"
|
|
154
|
+
await asyncio.sleep(1)
|
|
155
|
+
|
|
156
|
+
await asyncio.gather(work("a"), work("b"))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Cost
|
|
160
|
+
|
|
161
|
+
| | per span |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| default | ~13 µs |
|
|
164
|
+
| `spans=False` | ~6 µs |
|
|
165
|
+
| no active run | ~3 µs |
|
|
166
|
+
|
|
167
|
+
For comparison, `et.log()` is ~24 µs. Twenty spans on a 100 ms step is 0.26% of
|
|
168
|
+
the step. If your step is closer to a millisecond, set `spans=False` and keep the
|
|
169
|
+
metrics, or time fewer regions.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Streams
|
|
2
|
+
|
|
3
|
+
A training loop and a data worker measure different things on different clocks:
|
|
4
|
+
training step 100 and data batch 100 are unrelated. A **stream** gives each
|
|
5
|
+
producer its own step cursor and its own file inside one run directory.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# the training process
|
|
9
|
+
et.init(project="llm", name="sft-1")
|
|
10
|
+
et.log({"train/loss": loss}) # steps 0, 1, 2, ...
|
|
11
|
+
|
|
12
|
+
# the data worker, a separate process
|
|
13
|
+
et.init(project="llm", name="sft-1", stream="data")
|
|
14
|
+
et.log({"data/produce_ms": 12.4}) # its own steps 0, 1, 2, ...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Without this, both producers share one cursor: their steps interleave, the merge
|
|
18
|
+
puts unrelated metrics on one row, and `step_policy="monotonic"` drops whichever
|
|
19
|
+
producer happens to fall behind.
|
|
20
|
+
|
|
21
|
+
## Layout
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
runs/llm/sft-1/
|
|
25
|
+
├── metrics.jsonl # the default producer
|
|
26
|
+
├── metrics.data.jsonl # stream "data"
|
|
27
|
+
├── metrics.meta.json
|
|
28
|
+
├── metrics.data.meta.json
|
|
29
|
+
├── summary.json # per stream, so processes cannot clobber each other
|
|
30
|
+
├── summary.data.json
|
|
31
|
+
├── config.json
|
|
32
|
+
└── config.data.json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Streams compose with [rank shards](distributed.md): rank 1 of the data worker
|
|
36
|
+
writes `metrics.data.rank1.jsonl`.
|
|
37
|
+
|
|
38
|
+
Stream names become part of a filename, so they must be letters, digits, `_` or
|
|
39
|
+
`-`, starting with a letter or digit. `rank1` and friends are rejected because
|
|
40
|
+
they already mean a rank shard.
|
|
41
|
+
|
|
42
|
+
## Reading
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
et.history(50) # whichever stream this process writes
|
|
46
|
+
et.history(50, stream=None) # the default producer
|
|
47
|
+
et.history(50, stream="data") # another stream
|
|
48
|
+
et.history(50, run=path, stream="data") # offline
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Omitting `stream` reads the running stream; passing `stream=None` explicitly means
|
|
52
|
+
the default, unnamed one. Another process's stream is read from its file, so you
|
|
53
|
+
see what it has flushed rather than what it has buffered.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from expr_tracker.history import list_streams
|
|
57
|
+
list_streams("runs/llm/sft-1") # [None, "data"]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Alerts
|
|
61
|
+
|
|
62
|
+
Each process alerts on what it can see, which is its own stream. That is usually
|
|
63
|
+
what you want — the data worker is the thing that knows its pipeline stalled:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
et.init(
|
|
67
|
+
project="llm", name="sft-1", stream="data",
|
|
68
|
+
alert_rules=[
|
|
69
|
+
"produce_ms > 1000 => warning: data pipeline slow",
|
|
70
|
+
"no_data(5m) => error: data worker stopped producing",
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
!!! note
|
|
76
|
+
A rule cannot span streams that live in different processes, because neither
|
|
77
|
+
process holds the other's metrics. If you need that, log both from one
|
|
78
|
+
process, or evaluate the rule downstream against the files.
|
|
79
|
+
|
|
80
|
+
## Backends
|
|
81
|
+
|
|
82
|
+
A stream is forwarded as its own backend run, grouped under the run name:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
et.init(project="llm", name="sft-1", stream="data", backends=["wandb"])
|
|
86
|
+
# -> wandb.init(name="sft-1-data", group="sft-1", job_type="data")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Both wandb and trackio understand `group`, and neither can merge two step axes
|
|
90
|
+
into a single run. wandb's shared mode can, but it needs a live server and has no
|
|
91
|
+
trackio equivalent, so grouping is the default.
|
|
92
|
+
|
|
93
|
+
Override it per backend if you want something else:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
et.init(..., stream="data", backend_kwargs={"wandb": {"group": "my-group"}})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## When you do not need a stream
|
|
100
|
+
|
|
101
|
+
If the producers are in **one process at different cadences** — an eval loop
|
|
102
|
+
every 100 steps, say — you do not need a stream. Log with the training step and
|
|
103
|
+
the sparse metric simply appears on the steps where you logged it:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
et.log({"train/loss": loss})
|
|
107
|
+
if step % 100 == 0:
|
|
108
|
+
et.log({"eval/acc": acc}, step=step)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Window functions already work on that, because `eval.acc[20]` counts points of
|
|
112
|
+
that metric, not rows. Reach for a stream when producers are genuinely concurrent
|
|
113
|
+
and their step numbers mean different things.
|
|
@@ -23,6 +23,7 @@ et.init(
|
|
|
23
23
|
backends: Sequence[str] = ("wandb", "jsonl"),
|
|
24
24
|
backend_kwargs: dict[str, dict] | None = None,
|
|
25
25
|
print_to_screen: bool = False,
|
|
26
|
+
stream: str | None = None,
|
|
26
27
|
alert=None,
|
|
27
28
|
alert_rules: Sequence = (),
|
|
28
29
|
**history_options,
|
|
@@ -77,9 +78,13 @@ et.history(
|
|
|
77
78
|
fill_missing: bool = False,
|
|
78
79
|
dropna: bool = False,
|
|
79
80
|
run: str | Path | None = None,
|
|
81
|
+
stream: str | None = ...,
|
|
80
82
|
)
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
Omit `stream` to read the running one; pass `None` for the default producer. See
|
|
86
|
+
[Streams](../guide/streams.md).
|
|
87
|
+
|
|
83
88
|
`n=-1` or `None` returns everything. With `run=`, reads that run offline and no
|
|
84
89
|
`init()` is needed. See [Querying history](../guide/history.md).
|
|
85
90
|
|
|
@@ -148,6 +153,27 @@ et.register_backend(kind: str, cls: type[AlertBackend])
|
|
|
148
153
|
|
|
149
154
|
Registers a custom channel type.
|
|
150
155
|
|
|
156
|
+
## Spans
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
with et.span(name, **attributes) as span: ... # also async, also a decorator
|
|
160
|
+
span = et.start_span(name, **attributes) # ends with span.end()
|
|
161
|
+
span.set(**attributes)
|
|
162
|
+
span.duration_ms
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A closed span adds `<path>/duration_ms` and `<path>/count` to the open row and
|
|
166
|
+
appends the full record to `spans.jsonl`. See [Spans](../guide/spans.md).
|
|
167
|
+
|
|
168
|
+
### Trace export
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from expr_tracker.trace import build_trace, write_trace
|
|
172
|
+
|
|
173
|
+
write_trace(run, "trace.json", stream="*", step_range=None) # returns the span count
|
|
174
|
+
build_trace(run, stream="*") # the dict, unwritten
|
|
175
|
+
```
|
|
176
|
+
|
|
151
177
|
## Artifacts
|
|
152
178
|
|
|
153
179
|
```python
|