expr-tracker 0.2.3__tar.gz → 0.2.4__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.3 → expr_tracker-0.2.4}/PKG-INFO +7 -3
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/README.md +2 -1
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/design.md +27 -0
- expr_tracker-0.2.4/docs/guide/spans.md +289 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/reference/api.md +25 -4
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/reference/configuration.md +2 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/pyproject.toml +2 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/__init__.py +2 -1
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/store.py +4 -0
- expr_tracker-0.2.4/src/expr_tracker/plugins.py +269 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/spans.py +142 -14
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/trace.py +1 -0
- expr_tracker-0.2.4/tests/test_span_plugins.py +1163 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/uv.lock +16 -1
- expr_tracker-0.2.3/docs/guide/spans.md +0 -169
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/.github/workflows/docs.yaml +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/.github/workflows/release.yaml +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/.gitignore +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/LICENSE +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/architecture.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/getting-started.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/alerts.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/artifacts.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/backends.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/cli.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/distributed.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/history.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/logging.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/guide/streams.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/index.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/docs/reference/expressions.md +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/mkdocs.yml +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/_compat.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/__init__.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/backends/__init__.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/backends/base.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/dispatch.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/engine.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/__init__.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/eval.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/functions.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/lexer.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/nodes.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/parser.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/expr/rule.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/alerts/models.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/artifacts.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/cli.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/encoders.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/__init__.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/codec.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/frame.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/naming.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/reader.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/series.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/history/writer.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/py.typed +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/run.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/summary.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/tracker.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/src/expr_tracker/types.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/conftest.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_alert_backends.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_alert_delivery.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_alert_dispatch.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_alert_engine.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_alert_models.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_alert_routing.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_artifacts.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_benchmark.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_cache.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_cli.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_correctness.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_distributed.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_e2e.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_expr_builder.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_expr_eval.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_expr_functions.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_expr_parser.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_expr_properties.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_failure_modes.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_features.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_frame_codec_summary.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_history.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_hot_paths.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_integration.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_lark_live.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_perf.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_public_surfaces.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_review_regressions.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_rule_lifecycle.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_run_backends.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_scenarios.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_spans.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_step_commit.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_streams.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_stress.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_trace.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_trackio.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_value_encoding.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_wandb.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_writer_buffer.py +0 -0
- {expr_tracker-0.2.3 → expr_tracker-0.2.4}/tests/test_writer_durability.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: expr_tracker
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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/
|
|
@@ -28,11 +28,14 @@ Requires-Dist: click>=8.1.0
|
|
|
28
28
|
Requires-Dist: loguru>=0.7.3
|
|
29
29
|
Requires-Dist: pydantic>=2.0
|
|
30
30
|
Provides-Extra: all
|
|
31
|
+
Requires-Dist: nvidia-ml-py>=12.0; extra == 'all'
|
|
31
32
|
Requires-Dist: pandas>=1.5; extra == 'all'
|
|
32
33
|
Requires-Dist: polars>=0.20; extra == 'all'
|
|
33
34
|
Requires-Dist: slark>=0.1.28; extra == 'all'
|
|
34
35
|
Requires-Dist: trackio>=0.4.0; extra == 'all'
|
|
35
36
|
Requires-Dist: wandb>=0.21.0; extra == 'all'
|
|
37
|
+
Provides-Extra: gpu
|
|
38
|
+
Requires-Dist: nvidia-ml-py>=12.0; extra == 'gpu'
|
|
36
39
|
Provides-Extra: lark
|
|
37
40
|
Requires-Dist: slark>=0.1.28; extra == 'lark'
|
|
38
41
|
Provides-Extra: pandas
|
|
@@ -106,7 +109,7 @@ install command; it never crashes a run.
|
|
|
106
109
|
| [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. |
|
|
107
110
|
| [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. |
|
|
108
111
|
| [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. |
|
|
112
|
+
| [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. |
|
|
110
113
|
| [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. |
|
|
111
114
|
| [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. |
|
|
112
115
|
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et rules explain`, `et rules test`, `et alert`. |
|
|
@@ -152,6 +155,7 @@ uv run ruff format src tests
|
|
|
152
155
|
| `test_expr_properties.py` | DSL properties: render round-trip stability, precedence, the whole `M` builder |
|
|
153
156
|
| `test_trace.py` | Chrome Trace export: lane layout, stream and step selection, the CLI |
|
|
154
157
|
| `test_spans.py` | nesting, aggregation, decorator and async forms, errors, thread and task isolation |
|
|
158
|
+
| `test_span_plugins.py` | `print_fn` output and indentation, the plugin protocol, failure isolation, CPU/GPU built-ins |
|
|
155
159
|
| `test_streams.py` | stream naming and validation, isolation, resolution order, backend grouping, two-process runs |
|
|
156
160
|
| `test_distributed.py` | rank shards, `alert_on_rank`, real multi-process runs |
|
|
157
161
|
| `test_wandb.py` | real wandb in offline mode: parameter mapping, step alignment, artifacts |
|
|
@@ -59,7 +59,7 @@ install command; it never crashes a run.
|
|
|
59
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. |
|
|
60
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. |
|
|
61
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. |
|
|
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. `print_fn` prints the tree live, and plugins attach CPU and GPU cost to each region. |
|
|
63
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. |
|
|
64
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. |
|
|
65
65
|
| [CLI](https://hspk.github.io/expr_tracker/guide/cli/) | `et history`, `et rules explain`, `et rules test`, `et alert`. |
|
|
@@ -105,6 +105,7 @@ uv run ruff format src tests
|
|
|
105
105
|
| `test_expr_properties.py` | DSL properties: render round-trip stability, precedence, the whole `M` builder |
|
|
106
106
|
| `test_trace.py` | Chrome Trace export: lane layout, stream and step selection, the CLI |
|
|
107
107
|
| `test_spans.py` | nesting, aggregation, decorator and async forms, errors, thread and task isolation |
|
|
108
|
+
| `test_span_plugins.py` | `print_fn` output and indentation, the plugin protocol, failure isolation, CPU/GPU built-ins |
|
|
108
109
|
| `test_streams.py` | stream naming and validation, isolation, resolution order, backend grouping, two-process runs |
|
|
109
110
|
| `test_distributed.py` | rank shards, `alert_on_rank`, real multi-process runs |
|
|
110
111
|
| `test_wandb.py` | real wandb in offline mode: parameter mapping, step alignment, artifacts |
|
|
@@ -111,11 +111,38 @@ so timing a region adds no row. The tree, with timestamps and attributes, goes t
|
|
|
111
111
|
`spans[.stream][.rankN].jsonl` through a second `JsonlWriter`, enqueued rather
|
|
112
112
|
than appended so a span does not pay for a flush decision of its own.
|
|
113
113
|
|
|
114
|
+
Plugins hang off the same two hooks. `start` runs before the clock starts and
|
|
115
|
+
`end` after it stops, so a plugin never inflates the duration it reports on; what
|
|
116
|
+
`end` returns is merged into the span's metrics under the span's own path, which
|
|
117
|
+
means a plugin needs no knowledge of the store, the codec or the accumulate rule.
|
|
118
|
+
A plugin that raises is logged and dropped: a measurement must not break the
|
|
119
|
+
thing it measures.
|
|
120
|
+
|
|
121
|
+
Peak-memory plugins have to fight the fact that `torch.cuda.max_memory_allocated`
|
|
122
|
+
is one global high-water mark. Resetting it per span is the only way to attribute
|
|
123
|
+
a peak, but a child's reset destroys its parent's history. `TorchMemory` keeps a
|
|
124
|
+
per-thread stack and, as each span closes, hands its parent back the peak the
|
|
125
|
+
parent had reached before that child opened, so `max(counter, carried)` is
|
|
126
|
+
correct at every level.
|
|
127
|
+
|
|
128
|
+
GPU utilisation cannot be attributed by reading NVML at a span boundary: the
|
|
129
|
+
counter is a rolling average over NVML's own window, so for a 3 ms span it
|
|
130
|
+
describes the wrong interval entirely. `GpuStats` samples in a background thread
|
|
131
|
+
and each span averages the samples that fall inside it, which also keeps the
|
|
132
|
+
NVML call off the measured path.
|
|
133
|
+
|
|
114
134
|
The nesting stack is a `ContextVar`, not a `threading.local`: a new thread starts
|
|
115
135
|
from the default and each asyncio task gets its own copy, which is exactly the
|
|
116
136
|
scoping spans need. (The run singleton went the other way, from `ContextVar` to a
|
|
117
137
|
locked global, because there the goal was for worker threads to *share* it.)
|
|
118
138
|
|
|
139
|
+
Closing a span filters itself out of the stack rather than resetting the token
|
|
140
|
+
it got when it opened. A token restores the entire stack as it was, so with
|
|
141
|
+
`start_span` — where spans need not close in the order they opened — an outer
|
|
142
|
+
span closing first would be put back by its inner span's reset, and everything
|
|
143
|
+
afterwards would nest under a span that had already ended. Filtering is exact,
|
|
144
|
+
and at ten levels deep it still does not show up against the ~15 µs a span costs.
|
|
145
|
+
|
|
119
146
|
### A.6 Multiple processes
|
|
120
147
|
|
|
121
148
|
The supported model is **rank 0 tracks**; there is no cross-rank merging.
|
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
## Printing a span as it runs
|
|
160
|
+
|
|
161
|
+
Pass `print_fn` and the span announces itself, indented one tab per level:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
with et.span("step", print_fn=print):
|
|
165
|
+
with et.span("forward"):
|
|
166
|
+
with et.span("attention"):
|
|
167
|
+
...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
-> step 16:41:38
|
|
172
|
+
-> forward 16:41:38
|
|
173
|
+
-> attention 16:41:38
|
|
174
|
+
<- attention 16:41:38 3.074ms
|
|
175
|
+
<- forward 16:41:38 5.481ms
|
|
176
|
+
<- step 16:41:38 9.641ms
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Children inherit the handler, so one argument on the outermost span prints the
|
|
180
|
+
whole tree. A child that passes its own `print_fn` takes over its subtree; pass
|
|
181
|
+
`print_fn=lambda line: None` to silence one. A failed span ends with `!ValueError`.
|
|
182
|
+
|
|
183
|
+
Indentation is relative to the span that started printing, not to absolute
|
|
184
|
+
nesting depth, so turning printing on deep inside a call stack still gives you a
|
|
185
|
+
tree rooted at the left margin. A new thread begins its own tree, because the
|
|
186
|
+
nesting stack does not cross threads. Concurrent asyncio tasks share one handler
|
|
187
|
+
and interleave their lines; each is still indented by its own depth, and the
|
|
188
|
+
`->` and `<-` markers pair them up.
|
|
189
|
+
|
|
190
|
+
Any callable taking one string works — `print`, `logger.info`, a list's `append`
|
|
191
|
+
in tests. It is called on the thread that ran the span, and an exception in it is
|
|
192
|
+
logged and swallowed.
|
|
193
|
+
|
|
194
|
+
Set it for the whole run instead of per span with `span_print_fn`:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
et.init(project="demo", span_print_fn=logger.info)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Plugins
|
|
201
|
+
|
|
202
|
+
A plugin measures a resource across a span. It is any object with
|
|
203
|
+
`start(span)` and `end(span) -> dict`; a plain `fn(span) -> dict` is an
|
|
204
|
+
end-only plugin. Whatever `end` returns is merged into the span's metrics under
|
|
205
|
+
the span's own path:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from expr_tracker.plugins import CpuTime, GpuStats, TorchMemory
|
|
209
|
+
|
|
210
|
+
with et.span("forward", plugins=[CpuTime(), TorchMemory()]):
|
|
211
|
+
loss = model(batch)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
et.history(1)[0]
|
|
216
|
+
# {"forward/duration_ms": 41.2, "forward/count": 1,
|
|
217
|
+
# "forward/cpu_percent": 101.2, "forward/cpu_time_ms": 41.4,
|
|
218
|
+
# "forward/gpu_mem_peak_mb": 456.1, "forward/gpu_mem_delta_mb": 8.1, ...}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Like `print_fn`, plugins are inherited by children, can be overridden per span,
|
|
222
|
+
and can be set run-wide with `span_plugins`. They are opt-in because they are not
|
|
223
|
+
free. A plugin that raises is logged and skipped; it never breaks the span or the
|
|
224
|
+
code being measured, and a plugin that fails on every span is logged only once.
|
|
225
|
+
Plugin metrics also reach `spans.jsonl` and show up in the `et trace` viewer
|
|
226
|
+
under a span's *Arguments*.
|
|
227
|
+
|
|
228
|
+
`duration_ms` and `count` belong to the span. A plugin returning either has that
|
|
229
|
+
key dropped with a warning, so nothing can quietly redefine what a span measured.
|
|
230
|
+
|
|
231
|
+
### Built-ins
|
|
232
|
+
|
|
233
|
+
| plugin | metrics | needs |
|
|
234
|
+
| --- | --- | --- |
|
|
235
|
+
| `CpuTime()` | `cpu_time_ms`, `cpu_percent` | nothing |
|
|
236
|
+
| `TorchMemory(device=None)` | `gpu_mem_peak_mb`, `gpu_mem_delta_mb` | `torch` + CUDA |
|
|
237
|
+
| `GpuStats(index=0, interval=0.1)` | `gpu_percent`, `gpu_mem_used_mb` | `pip install expr-tracker[gpu]` |
|
|
238
|
+
|
|
239
|
+
`CpuTime` divides process CPU time by wall time, so `cpu_percent` above 100 means
|
|
240
|
+
the span used more than one core. It counts every thread in the process, so
|
|
241
|
+
unrelated concurrent work inflates it, and it is noise on spans under a
|
|
242
|
+
millisecond.
|
|
243
|
+
|
|
244
|
+
`TorchMemory` reports what your own allocator held, which is the number that
|
|
245
|
+
predicts an OOM. `gpu_mem_peak_mb` is absolute, so memory a tensor was already
|
|
246
|
+
holding when the span opened counts towards it; `gpu_mem_delta_mb` is what the
|
|
247
|
+
span kept. `torch.cuda.max_memory_allocated` is a single global high-water mark,
|
|
248
|
+
so a child span resetting it would erase its parent's history — the plugin keeps
|
|
249
|
+
its own stack and hands each parent back the peak it had reached before the child
|
|
250
|
+
opened.
|
|
251
|
+
|
|
252
|
+
`GpuStats` reads the whole device, including other processes. A background thread
|
|
253
|
+
samples NVML every `interval`, and a span reports the mean utilisation and the
|
|
254
|
+
peak memory over its own window, so the span itself pays almost nothing. Spans
|
|
255
|
+
shorter than `interval` contain no sample and fall back to one live reading,
|
|
256
|
+
which for utilisation is NVML's own rolling average rather than a measurement of
|
|
257
|
+
that span. Samplers are shared across plugins with the same device and interval.
|
|
258
|
+
|
|
259
|
+
### CUDA is asynchronous
|
|
260
|
+
|
|
261
|
+
A span measures the wall time of the Python block, and CUDA kernels are queued,
|
|
262
|
+
not awaited. A span around a launch can close in microseconds while the GPU is
|
|
263
|
+
still busy, and the wait then lands on whatever later line synchronises. If you
|
|
264
|
+
want a span to mean GPU time, synchronise inside it:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
with et.span("forward", plugins=[TorchMemory()]):
|
|
268
|
+
out = model(batch)
|
|
269
|
+
torch.cuda.synchronize()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`gpu_mem_peak_mb` is unaffected: allocator bookkeeping is recorded at launch.
|
|
273
|
+
|
|
274
|
+
## Cost
|
|
275
|
+
|
|
276
|
+
| | per span |
|
|
277
|
+
| --- | --- |
|
|
278
|
+
| default | ~15 µs |
|
|
279
|
+
| `spans=False` | ~6 µs |
|
|
280
|
+
| no active run | ~3 µs |
|
|
281
|
+
| `print_fn` | +6 µs |
|
|
282
|
+
| `CpuTime()` | +8 µs |
|
|
283
|
+
| `GpuStats()` | +4 µs, or +36 µs on a span shorter than `interval` |
|
|
284
|
+
| `TorchMemory()` | +62 µs |
|
|
285
|
+
|
|
286
|
+
For comparison, `et.log()` is ~24 µs. Twenty spans on a 100 ms step is 0.3% of
|
|
287
|
+
the step. If your step is closer to a millisecond, set `spans=False` and keep the
|
|
288
|
+
metrics, or time fewer regions. Put `TorchMemory` on the few spans whose memory
|
|
289
|
+
you actually care about rather than run-wide.
|
|
@@ -156,14 +156,35 @@ Registers a custom channel type.
|
|
|
156
156
|
## Spans
|
|
157
157
|
|
|
158
158
|
```python
|
|
159
|
-
with et.span(name, **attributes) as span: ...
|
|
160
|
-
span = et.start_span(name, **attributes)
|
|
159
|
+
with et.span(name, print_fn=None, plugins=(), **attributes) as span: ...
|
|
160
|
+
span = et.start_span(name, print_fn=None, plugins=(), **attributes)
|
|
161
161
|
span.set(**attributes)
|
|
162
162
|
span.duration_ms
|
|
163
|
+
span.metrics # what the plugins measured
|
|
163
164
|
```
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
`et.span` is also an async context manager and a decorator. A closed span adds
|
|
167
|
+
`<path>/duration_ms`, `<path>/count` and one key per plugin metric to the open
|
|
168
|
+
row, and appends the full record to `spans.jsonl`.
|
|
169
|
+
|
|
170
|
+
`print_fn(line)` announces the span's start and end, indented one tab per level.
|
|
171
|
+
`plugins` measure a resource across the span; both are inherited by child spans
|
|
172
|
+
and default to the run's `span_print_fn` and `span_plugins`.
|
|
173
|
+
|
|
174
|
+
### Plugins
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from expr_tracker.plugins import CpuTime, GpuStats, TorchMemory
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| plugin | metrics |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `CpuTime()` | `cpu_time_ms`, `cpu_percent` |
|
|
183
|
+
| `TorchMemory(device=None)` | `gpu_mem_peak_mb`, `gpu_mem_delta_mb` |
|
|
184
|
+
| `GpuStats(index=0, interval=0.1)` | `gpu_percent`, `gpu_mem_used_mb` |
|
|
185
|
+
|
|
186
|
+
Any object with `start(span)` and `end(span) -> dict` is a plugin, as is a plain
|
|
187
|
+
`fn(span) -> dict`. See [Spans](../guide/spans.md).
|
|
167
188
|
|
|
168
189
|
### Trace export
|
|
169
190
|
|
|
@@ -15,6 +15,8 @@ list of valid options rather than being silently ignored.
|
|
|
15
15
|
| `rank_aware` | `True` | non-zero ranks write their own shard |
|
|
16
16
|
| `stream` | `None` | an independent producer with its own file and step cursor |
|
|
17
17
|
| `spans` | `True` | write the span tree to `spans.jsonl` beside the metrics |
|
|
18
|
+
| `span_print_fn` | `None` | default `print_fn` for every span in the run |
|
|
19
|
+
| `span_plugins` | `()` | default plugins for every span in the run |
|
|
18
20
|
| `print_to_screen` | `False` | print every committed row |
|
|
19
21
|
| `print_handle` | `print` | where those lines go |
|
|
20
22
|
| `buffer_size` | 50 | flush after this many buffered rows |
|
|
@@ -45,12 +45,14 @@ wandb = ["wandb>=0.21.0"]
|
|
|
45
45
|
trackio = ["trackio>=0.4.0"]
|
|
46
46
|
pandas = ["pandas>=1.5"]
|
|
47
47
|
polars = ["polars>=0.20"]
|
|
48
|
+
gpu = ["nvidia-ml-py>=12.0"]
|
|
48
49
|
all = [
|
|
49
50
|
"slark>=0.1.28",
|
|
50
51
|
"wandb>=0.21.0",
|
|
51
52
|
"trackio>=0.4.0",
|
|
52
53
|
"pandas>=1.5",
|
|
53
54
|
"polars>=0.20",
|
|
55
|
+
"nvidia-ml-py>=12.0",
|
|
54
56
|
]
|
|
55
57
|
|
|
56
58
|
[project.urls]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError
|
|
2
2
|
from importlib.metadata import version as _version
|
|
3
3
|
|
|
4
|
-
from . import tracker
|
|
4
|
+
from . import plugins, tracker
|
|
5
5
|
from .alerts import (
|
|
6
6
|
AlertConfig,
|
|
7
7
|
AlertLevel,
|
|
@@ -61,6 +61,7 @@ __all__ = [
|
|
|
61
61
|
"list_alert_rules",
|
|
62
62
|
"log",
|
|
63
63
|
"log_artifact",
|
|
64
|
+
"plugins",
|
|
64
65
|
"register_backend",
|
|
65
66
|
"remove_alert_rule",
|
|
66
67
|
"span",
|
|
@@ -19,6 +19,7 @@ from collections import deque
|
|
|
19
19
|
from collections.abc import Callable, Sequence
|
|
20
20
|
from dataclasses import dataclass, fields
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
22
23
|
|
|
23
24
|
from loguru import logger
|
|
24
25
|
|
|
@@ -74,6 +75,9 @@ class HistoryOptions:
|
|
|
74
75
|
stream: str | None = None
|
|
75
76
|
# write the full span tree to spans.jsonl beside the metrics
|
|
76
77
|
spans: bool = True
|
|
78
|
+
# run-wide defaults for et.span(); a span's own arguments win
|
|
79
|
+
span_print_fn: Callable[[str], None] | None = None
|
|
80
|
+
span_plugins: Sequence[Any] = ()
|
|
77
81
|
# alerts and output
|
|
78
82
|
alert_window: int = DEFAULT_WINDOW
|
|
79
83
|
print_to_screen: bool = False
|