expr-tracker 0.3.5__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/PKG-INFO +1 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/architecture.md +32 -9
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/design.md +22 -17
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/artifacts.md +3 -0
- expr_tracker-0.4.0/docs/guide/logging.md +180 -0
- expr_tracker-0.4.0/docs/guide/spans.md +457 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/streams.md +32 -4
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/reference/api.md +35 -3
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/reference/configuration.md +32 -4
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/reference/expressions.md +8 -1
- expr_tracker-0.4.0/src/expr_tracker/_process.py +33 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/__init__.py +12 -4
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/backends/base.py +3 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/dispatch.py +21 -2
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/eval.py +6 -2
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/rule.py +25 -12
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/models.py +9 -4
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/artifacts.py +9 -3
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/cli.py +57 -7
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/encoders.py +33 -2
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/codec.py +14 -22
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/frame.py +6 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/reader.py +212 -52
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/series.py +37 -2
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/store.py +473 -170
- expr_tracker-0.4.0/src/expr_tracker/history/writer.py +768 -0
- expr_tracker-0.4.0/src/expr_tracker/plugins.py +392 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/run.py +75 -9
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/spans.py +57 -14
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/summary.py +12 -1
- expr_tracker-0.4.0/src/expr_tracker/trace.py +533 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/tracker.py +16 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/conftest.py +1 -3
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_alert_routing.py +0 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_benchmark.py +81 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_cache.py +1 -2
- expr_tracker-0.4.0/tests/test_callback_isolation.py +863 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_cli.py +140 -3
- expr_tracker-0.4.0/tests/test_commit_timestamps.py +131 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_correctness.py +0 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_distributed.py +0 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_e2e.py +77 -8
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_expr_properties.py +0 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_failure_modes.py +0 -1
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_features.py +0 -2
- expr_tracker-0.4.0/tests/test_fork_safety.py +204 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_history.py +18 -21
- expr_tracker-0.4.0/tests/test_history_admission.py +186 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_hot_paths.py +0 -1
- expr_tracker-0.4.0/tests/test_nonfinite_conditions.py +56 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_perf.py +0 -1
- expr_tracker-0.4.0/tests/test_process_compat.py +34 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_public_surfaces.py +0 -1
- expr_tracker-0.4.0/tests/test_reader_edge_cases.py +830 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_review_regressions.py +7 -20
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_rule_lifecycle.py +0 -2
- expr_tracker-0.4.0/tests/test_rule_overrides.py +69 -0
- expr_tracker-0.4.0/tests/test_rule_quoted_names.py +40 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_run_backends.py +25 -6
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_run_dir.py +1 -3
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_scenarios.py +0 -5
- expr_tracker-0.4.0/tests/test_serialization_edge_cases.py +496 -0
- expr_tracker-0.4.0/tests/test_series_edge_cases.py +115 -0
- expr_tracker-0.4.0/tests/test_span_lifecycle.py +496 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_span_plugins.py +819 -13
- expr_tracker-0.4.0/tests/test_span_value_encoding.py +111 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_spans.py +159 -27
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_step_commit.py +94 -7
- expr_tracker-0.4.0/tests/test_step_types.py +81 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_streams.py +1 -10
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_stress.py +4 -4
- expr_tracker-0.4.0/tests/test_trace.py +853 -0
- expr_tracker-0.4.0/tests/test_trace_perf.py +230 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_trackio.py +2 -1
- expr_tracker-0.4.0/tests/test_unicode_records.py +126 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_value_encoding.py +1 -5
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_wandb.py +3 -4
- expr_tracker-0.4.0/tests/test_writer_async.py +764 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_writer_buffer.py +1 -1
- expr_tracker-0.4.0/tests/test_writer_bytes.py +635 -0
- expr_tracker-0.4.0/tests/test_writer_diagnostics.py +556 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_writer_durability.py +0 -1
- expr_tracker-0.3.5/docs/guide/logging.md +0 -112
- expr_tracker-0.3.5/docs/guide/spans.md +0 -307
- expr_tracker-0.3.5/src/expr_tracker/history/writer.py +0 -410
- expr_tracker-0.3.5/src/expr_tracker/plugins.py +0 -269
- expr_tracker-0.3.5/src/expr_tracker/trace.py +0 -190
- expr_tracker-0.3.5/tests/test_trace.py +0 -421
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/.github/workflows/ci.yaml +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/.github/workflows/docs.yaml +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/.github/workflows/release.yaml +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/.gitignore +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/LICENSE +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/README.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/examples.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/getting-started.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/alerts.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/backends.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/cli.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/distributed.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/guide/history.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/docs/index.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/README.md +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/alert_rules.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/checkpoints.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/early_stopping.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/multiprocess_pipeline.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/profile_step.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/examples/quickstart.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/mkdocs.yml +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/pyproject.toml +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/__init__.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/_compat.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/backends/__init__.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/backends/cards.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/engine.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/__init__.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/functions.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/lexer.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/nodes.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/alerts/expr/parser.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/__init__.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/history/naming.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/py.typed +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/src/expr_tracker/types.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_alert_backends.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_alert_delivery.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_alert_dispatch.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_alert_engine.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_alert_models.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_artifacts.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_examples.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_expr_builder.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_expr_eval.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_expr_functions.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_expr_parser.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_frame_codec_summary.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_integration.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/tests/test_lark.py +0 -0
- {expr_tracker-0.3.5 → expr_tracker-0.4.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: expr_tracker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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/
|
|
@@ -58,7 +58,7 @@ Run.log ──> Summary.observe(data) # last value per metric
|
|
|
58
58
|
└──> HistoryStore.log
|
|
59
59
|
│ encode once (jsonable_encoder)
|
|
60
60
|
│ merge into the open row for the current step
|
|
61
|
-
▼ commit when the step advances / commit=True / finish /
|
|
61
|
+
▼ commit when the step advances / commit=True / finish / flush(commit_open=True)
|
|
62
62
|
HistoryStore._emit
|
|
63
63
|
├── _store_row(record, line) (holds the store lock)
|
|
64
64
|
│ ├── cache.append((row, step, line)) # ordinal allocated here
|
|
@@ -125,12 +125,13 @@ plain functions over a sequence, so they hold no lock and are tested directly.
|
|
|
125
125
|
|
|
126
126
|
### Rows versus steps
|
|
127
127
|
|
|
128
|
-
A step normally occupies one physical row, but
|
|
129
|
-
|
|
128
|
+
A step normally occupies one physical row, but an explicit commit followed by
|
|
129
|
+
more data for that step writes a *patch line*, so one step can span several rows.
|
|
130
130
|
The two are kept strictly separate in the read path:
|
|
131
131
|
|
|
132
|
-
* `JsonlReader.tail_rows(n)` returns **physical rows**; `JsonlReader.tail(n)`
|
|
133
|
-
**
|
|
132
|
+
* `JsonlReader.tail_rows(n)` returns **physical rows**; `JsonlReader.tail(n)` selects
|
|
133
|
+
the most recently written **steps** and merges all their patches. Unique-step
|
|
134
|
+
files use reverse reads; arbitrary patch histories require scanning older data.
|
|
134
135
|
* `HistoryStore._collect_tail(steps)` works in rows, then `_take_steps` merges and
|
|
135
136
|
trims by *step*, so `get(n)` never returns a half-merged oldest row.
|
|
136
137
|
* The cache stores each row's step next to its bytes, so `_view_tail` snapshots the
|
|
@@ -156,16 +157,38 @@ Disk fallback only happens once rows have been evicted or the run was resumed;
|
|
|
156
157
|
|
|
157
158
|
| Lock / thread | Owner | Protects |
|
|
158
159
|
| --- | --- | --- |
|
|
159
|
-
| `HistoryStore.
|
|
160
|
+
| `HistoryStore._lifecycle_lock` | store | reinitialization and shutdown |
|
|
161
|
+
| `HistoryStore._write_lock` (RLock) | store | accepted write transactions and row admission |
|
|
162
|
+
| `HistoryStore._lock` (RLock) | store | open row, cache, series and row ordinal |
|
|
163
|
+
| notification condition | store | ordered commit tickets, delivery ownership and completion |
|
|
160
164
|
| `JsonlWriter._lock` (RLock) | writer | buffer, index, meta fields |
|
|
161
165
|
| `JsonlWriter._write_lock` | writer | batch swap + file append (ordering) |
|
|
162
|
-
|
|
|
163
|
-
| buffer `threading.Timer` | writer | flushes records that sat in memory too long |
|
|
166
|
+
| reusable writer worker | writer | asynchronous span writes and timed buffer flushing; drained at shutdown |
|
|
164
167
|
| `CompiledRule.lock` | engine | one rule's state machine transitions |
|
|
165
168
|
| dispatch worker thread | dispatcher | drains the send queue; drained at exit |
|
|
166
169
|
| watchdog thread | engine | evaluates time-based rules when no logs arrive |
|
|
167
170
|
|
|
168
|
-
|
|
171
|
+
Store writes enter the transaction lock before the store-state lock. Shutdown
|
|
172
|
+
rejects new writes, waits for accepted metric/span transactions, then flushes and
|
|
173
|
+
closes the writers before closing downstream sinks. Writer workers never acquire
|
|
174
|
+
store locks. Open rows have no timeout and no background commit thread.
|
|
175
|
+
Admission waits outside the store-state lock, so queue backpressure does not
|
|
176
|
+
block history reads. Rejected records do not consume cache row ordinals or reach
|
|
177
|
+
commit callbacks.
|
|
178
|
+
Read snapshots acquire the metric writer lock before the store-state lock, the
|
|
179
|
+
same order used when writer error handlers inspect history.
|
|
180
|
+
|
|
181
|
+
Commit notifications are staged in write order, then delivered after releasing
|
|
182
|
+
transaction/state locks. Committing callers wait for their own tickets; span-only
|
|
183
|
+
writes do not wait for unrelated callbacks. Delivery is serial and supports
|
|
184
|
+
recursive logging/flush, while lifecycle operations are rejected inside callbacks.
|
|
185
|
+
Reinitialization drains the old run's notifications before replacing its metadata.
|
|
186
|
+
Alert-series updates occur during ordered delivery, not admission, preventing
|
|
187
|
+
queued future rows from changing an earlier notification's scalar/window results.
|
|
188
|
+
|
|
189
|
+
Run/store/span/plugin objects are process-owned. Fork resets process-global
|
|
190
|
+
locks and the active run/stack in the child; inherited objects are rejected before
|
|
191
|
+
acquiring their locks, and inherited exit hooks do not flush parent data.
|
|
169
192
|
|
|
170
193
|
## Extension points
|
|
171
194
|
|
|
@@ -22,7 +22,7 @@ Three write stages:
|
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
log() ──merge──> open row (current step, in memory)
|
|
25
|
-
│ step advances / commit=True / flush / finish
|
|
25
|
+
│ step advances / commit=True / flush(commit_open=True) / finish
|
|
26
26
|
▼
|
|
27
27
|
write buffer (adaptive: buffer_size / buffer_interval / max_buffer_seconds)
|
|
28
28
|
│
|
|
@@ -45,10 +45,9 @@ log(metrics: dict, step: int | None = None, commit: bool | None = None)
|
|
|
45
45
|
- `step_policy="monotonic"` (default) drops backward writes with a warning;
|
|
46
46
|
`"allow"` accepts them and marks `meta.sorted = false`.
|
|
47
47
|
- Writing the same key twice in one step is last-wins, warned once per key per run.
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
and `meta.has_duplicate_steps` is set; the reader merges by step
|
|
51
|
-
unaffected.
|
|
48
|
+
- An open row has no time limit: elapsed time never commits it or advances the
|
|
49
|
+
step. If more data arrives for an explicitly committed step, it is written as a
|
|
50
|
+
**patch line** and `meta.has_duplicate_steps` is set; the reader merges by step.
|
|
52
51
|
|
|
53
52
|
**The writer guarantees one line per step in ascending order; the reader assumes
|
|
54
53
|
none of that** — it must cope with older files, stray writers and patch lines.
|
|
@@ -102,9 +101,11 @@ commit rules exist in one place regardless of which API produced the metrics.
|
|
|
102
101
|
Two differences from a log call:
|
|
103
102
|
|
|
104
103
|
- the merge **accumulates** rather than replaces, because the same span can run
|
|
105
|
-
many times in one step
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
many times in one step: durations and counters sum, memory peaks take the
|
|
105
|
+
maximum, and utilization uses a duration-weighted mean;
|
|
106
|
+
- built-in durations and counts are already floats and ints, so they skip
|
|
107
|
+
`RecordCodec`. Optional plugin measurements and attributes are encoded
|
|
108
|
+
independently, keeping bare spans cheap without accepting unserializable data.
|
|
108
109
|
|
|
109
110
|
Metrics are named `<measurement>/<span path>` — `time_ms/step/forward`, not
|
|
110
111
|
`step/forward/time_ms`. A tracker UI groups by the segment before the first
|
|
@@ -116,12 +117,16 @@ group and leaves the caller's names alone.
|
|
|
116
117
|
A span never commits a step: durations ride along with whatever `log()` commits,
|
|
117
118
|
so timing a region adds no row. The tree, with timestamps and attributes, goes to
|
|
118
119
|
`spans[.stream][.rankN].jsonl` through a second `JsonlWriter`, enqueued rather
|
|
119
|
-
than
|
|
120
|
+
than committed as metric rows. That writer's reusable background worker flushes completed
|
|
121
|
+
spans even if no further spans or metrics arrive, without advancing the step.
|
|
120
122
|
|
|
121
123
|
The committed history row is also the fan-out boundary. Summary, alerts and
|
|
122
124
|
remote backends consume the exact row emitted by `HistoryStore`, rather than the
|
|
123
125
|
individual `log()` calls that assembled it. Span and plugin metrics therefore
|
|
124
126
|
reach every sink without a second metric path.
|
|
127
|
+
Commit tickets preserve that order while callbacks execute outside the transaction
|
|
128
|
+
lock. A slow sink blocks its committing callers, not unrelated span transactions;
|
|
129
|
+
shutdown and reinitialization drain notifications before closing or replacing sinks.
|
|
125
130
|
|
|
126
131
|
Plugins hang off the same two hooks. `start` runs before the clock starts and
|
|
127
132
|
`end` after it stops, so a plugin never inflates the duration it reports on; what
|
|
@@ -130,12 +135,12 @@ means a plugin needs no knowledge of the store, the codec or the accumulate rule
|
|
|
130
135
|
A plugin that raises is logged and dropped: a measurement must not break the
|
|
131
136
|
thing it measures.
|
|
132
137
|
|
|
133
|
-
Peak-memory plugins have to
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
Peak-memory plugins have to account for `torch.cuda.max_memory_allocated` being a
|
|
139
|
+
global high-water mark. `TorchMemory` coordinates active spans per device and
|
|
140
|
+
preserves every existing span's peak before a new span resets the counter.
|
|
141
|
+
CPU measurements are paired by span identity rather than a thread stack.
|
|
142
|
+
NVML samplers update bounded per-active-span aggregates, so long spans retain
|
|
143
|
+
their full sampling window without keeping every historical sample.
|
|
139
144
|
|
|
140
145
|
GPU utilisation cannot be attributed by reading NVML at a span boundary: the
|
|
141
146
|
counter is a rolling average over NVML's own window, so for a 3 ms span it
|
|
@@ -451,8 +456,8 @@ Read these before changing the modules they mention.
|
|
|
451
456
|
dispatch does not advance the state.
|
|
452
457
|
9. **Delivery failures are only logged** (`fail_silently`); configuration errors
|
|
453
458
|
raise at configuration time.
|
|
454
|
-
10. **
|
|
455
|
-
|
|
459
|
+
10. **Elapsed time never commits an open row**: a training step can run arbitrarily
|
|
460
|
+
long. Writer timers only flush buffered records; they do not advance the step.
|
|
456
461
|
|
|
457
462
|
---
|
|
458
463
|
|
|
@@ -28,6 +28,9 @@ recorded against it.
|
|
|
28
28
|
|
|
29
29
|
Publishers sharing a store serialize version allocation, file copying, and index
|
|
30
30
|
updates with a filesystem lock, including when they run in separate processes.
|
|
31
|
+
On POSIX, filenames represented using Python's filesystem-surrogate characters
|
|
32
|
+
round-trip through artifact indexes and retain distinct digests from literal
|
|
33
|
+
backslash-escape names.
|
|
31
34
|
|
|
32
35
|
```python
|
|
33
36
|
a = et.log_artifact("ckpt.pt", name="model") # v0
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Logging metrics
|
|
2
|
+
|
|
3
|
+
## One step, one line
|
|
4
|
+
|
|
5
|
+
`log()` merges metrics into an **open row** for the current step. The row is written
|
|
6
|
+
when it is committed, so several `log()` calls for one step become one line:
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
et.log({"loss": 1.0}, step=5)
|
|
10
|
+
et.log({"acc": 0.9}, step=5) # merged into the same row
|
|
11
|
+
et.log({"loss": 0.9}, step=6) # the step advanced -> step 5 is committed
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This matters because training and evaluation usually run at different points in the
|
|
15
|
+
loop but describe the same step.
|
|
16
|
+
|
|
17
|
+
## Commit semantics
|
|
18
|
+
|
|
19
|
+
`log(data, step=None, commit=None)` mirrors `wandb.log`.
|
|
20
|
+
|
|
21
|
+
| Call | `commit` default | Behaviour |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `log(d)` | `True` | Merge, commit, advance the step |
|
|
24
|
+
| `log(d, commit=False)` | — | Merge only |
|
|
25
|
+
| `log(d, step=N)` | `False` | Commit when the step advances |
|
|
26
|
+
| `log(d, step=N, commit=True)` | — | Commit immediately |
|
|
27
|
+
|
|
28
|
+
An open row is also committed by `finish()`, by the process exit hook, and by
|
|
29
|
+
`run.history.flush(commit_open=True)`. It never commits just because time passes,
|
|
30
|
+
so a long training step stays on one row. A plain `run.history.flush()` only writes
|
|
31
|
+
already committed rows; it does not commit the open row.
|
|
32
|
+
|
|
33
|
+
`finish()` waits for accepted writes and their commit callbacks before closing
|
|
34
|
+
downstream sinks. Concurrent `run.finish()` callers wait for the same shutdown
|
|
35
|
+
completion; a failed shutdown is surfaced to waiters rather than reported as
|
|
36
|
+
success. Call it outside commit/print and asynchronous alert-delivery callbacks;
|
|
37
|
+
trying to finish or reinitialize from such a callback raises `RuntimeError` to
|
|
38
|
+
avoid reentrant shutdown.
|
|
39
|
+
Lifecycle calls are also rejected from writer diagnostic callbacks, history
|
|
40
|
+
writer workers, unfinished write transactions, or an already-running lifecycle
|
|
41
|
+
operation; use the caller thread after the logging operation returns.
|
|
42
|
+
Writer diagnostics are emitted after writer locks are released and accounting is
|
|
43
|
+
consistent, allowing error sinks to inspect history without locking out writers.
|
|
44
|
+
|
|
45
|
+
Commit callbacks run outside the write transaction lock. A slow backend or
|
|
46
|
+
metric-print callback therefore does not block another thread recording an
|
|
47
|
+
uncommitted span. Committing callers still wait for their own notifications,
|
|
48
|
+
which are delivered serially in commit order; recursive logging and flush are
|
|
49
|
+
supported. No asynchronous callback worker or unbounded work queue is created.
|
|
50
|
+
|
|
51
|
+
Delivery may occur on another committing caller's thread. Context variables
|
|
52
|
+
come from the originating commit, but callbacks must not assume thread identity.
|
|
53
|
+
`flush()` and `finish()` wait for outstanding notifications before returning.
|
|
54
|
+
Alert-series windows advance with notification delivery, so a newer queued
|
|
55
|
+
commit cannot leak into an earlier alert. Ordinary history queries can already
|
|
56
|
+
see those accepted rows. Recursive logging advances the live alert series; when
|
|
57
|
+
an outer callback resumes, it sees the nested delivered rows, not a frozen snapshot.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
et.log({"train/loss": 1.0}, commit=False)
|
|
61
|
+
et.log({"train/grad_norm": 2.0}, commit=False)
|
|
62
|
+
et.log({"lr": 3e-4}) # commits all three as one step
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Once an explicit step is open, subsequent calls without `step=` (including
|
|
66
|
+
completed spans) reuse it:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
et.log({"train/loss": 1.0}, step=100)
|
|
70
|
+
et.log({"lr": 3e-4}) # commits step 100; the next step is 101
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Timestamps and liveness
|
|
74
|
+
|
|
75
|
+
Every row's `_time` is a Unix timestamp in seconds, captured when its open row
|
|
76
|
+
was first created. Later logs merged into that row do not replace the timestamp,
|
|
77
|
+
and committing or flushing it does not change it.
|
|
78
|
+
|
|
79
|
+
`no_data(duration)` uses the actual time the latest metric row was accepted,
|
|
80
|
+
not its `_time`, so completing a long training step does not immediately look
|
|
81
|
+
stale. Uncommitted updates, trace-only events and rejected rows do not refresh
|
|
82
|
+
this commit clock. `age(metric)` uses the sample timestamp from the row instead.
|
|
83
|
+
On resume, an invalid or non-finite timestamp is skipped for alert-window
|
|
84
|
+
backfill with a warning, rather than preventing later valid samples from being
|
|
85
|
+
restored. The original history rows remain available for inspection.
|
|
86
|
+
|
|
87
|
+
## Step ordering
|
|
88
|
+
|
|
89
|
+
Steps accept Python integers and integer-index values such as NumPy integer
|
|
90
|
+
scalars. They are normalized to native integers before encoding. Strings and
|
|
91
|
+
floating-point steps are rejected before payload consumption or open-row mutation,
|
|
92
|
+
so an invalid step cannot poison later logs.
|
|
93
|
+
|
|
94
|
+
By default steps must not go backwards. A backward step is dropped with a warning and
|
|
95
|
+
reaches no sink — not the file, not the summary, not the alert rules, not wandb:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
et.log({"loss": 1.0}, step=10)
|
|
99
|
+
et.log({"loss": 2.0}, step=3) # dropped, with a warning
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Pass `step_policy="allow"` to keep out-of-order writes. The file then contains
|
|
103
|
+
repeated steps, and the reader merges them on the way out.
|
|
104
|
+
|
|
105
|
+
!!! note
|
|
106
|
+
With `step_policy="allow"`, `history(n)` returns the *n* most recently **written**
|
|
107
|
+
steps rather than the *n* highest-numbered ones. `history(-1)` and `step_range`
|
|
108
|
+
are always step-ordered. A positive `n` combined with `step_range` selects the
|
|
109
|
+
highest matching steps. Live and offline queries use the same selection rules.
|
|
110
|
+
|
|
111
|
+
Patch lines are merged in full even when separated by other steps or split across
|
|
112
|
+
cache and disk. Reads pin an append-prefix boundary and do not flush unrelated
|
|
113
|
+
span records or wait for commit callbacks. A damaged or unreadable metadata
|
|
114
|
+
sidecar produces a warning and a scan of the readable data file.
|
|
115
|
+
|
|
116
|
+
## What you can log
|
|
117
|
+
|
|
118
|
+
Anything JSON-serialisable, plus the things ML code actually holds:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
et.log({
|
|
122
|
+
"loss": np.float32(0.5), # numpy scalars -> python scalars
|
|
123
|
+
"grads": np.array([1.0, 2.0]), # arrays -> lists
|
|
124
|
+
"config": PydanticModel(), # pydantic models -> dicts
|
|
125
|
+
"when": datetime.now(), # datetime -> ISO string
|
|
126
|
+
"path": Path("ckpt.pt"), # Path -> str
|
|
127
|
+
"stage": Stage.TRAIN, # Enum -> its value
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`NaN` and `Infinity` are preserved as-is, because a NaN loss is a signal and
|
|
132
|
+
`isnan()` in an alert rule depends on it. Python and pandas read this back; strict
|
|
133
|
+
JSON parsers such as `jq` do not.
|
|
134
|
+
|
|
135
|
+
A value that cannot be encoded falls back to `repr()` with a one-time warning, per
|
|
136
|
+
field — one bad value never costs you the rest of the row.
|
|
137
|
+
JSON integers too large for floating-point alert buffers remain unchanged in
|
|
138
|
+
history. They register their metric name but are skipped by numeric series with
|
|
139
|
+
a warning; neighboring metrics and their alerts continue to work.
|
|
140
|
+
Filesystem-surrogate characters in strings and paths are written as JSON escapes
|
|
141
|
+
rather than invalid UTF-8, so they do not discard neighboring metrics. Ordinary
|
|
142
|
+
Unicode remains readable UTF-8. Config replacement is atomic: a failed write
|
|
143
|
+
preserves the previous config file.
|
|
144
|
+
|
|
145
|
+
Metric names are free-form. `/` is conventional for grouping (`train/loss`), and the
|
|
146
|
+
alert language accepts `train.loss` as a shorthand for it.
|
|
147
|
+
|
|
148
|
+
## Reserved keys
|
|
149
|
+
|
|
150
|
+
`_step` and `_time` are written by the tracker. If you log them they are ignored, with
|
|
151
|
+
a warning.
|
|
152
|
+
|
|
153
|
+
## The summary
|
|
154
|
+
|
|
155
|
+
Every metric's last value is tracked automatically. Explicit assignments win and are
|
|
156
|
+
never overwritten:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
et.log({"acc": 0.91})
|
|
160
|
+
et.summary()["best_acc"] = 0.93 # survives later log() calls
|
|
161
|
+
dict(et.summary()) # {"acc": 0.91, "best_acc": 0.93}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The summary is saved on `finish()` and by the exit hook if the process dies first.
|
|
165
|
+
|
|
166
|
+
## Printing to screen
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
et.init(..., print_to_screen=True) # print every committed row
|
|
170
|
+
et.init(..., print_to_screen=True, print_handle=logger.info)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Only committed rows are printed, so you see one line per step. A failing handle is
|
|
174
|
+
logged and never interrupts training.
|
|
175
|
+
|
|
176
|
+
## Cost
|
|
177
|
+
|
|
178
|
+
`log()` costs about 26 µs (p50) without rules and 74 µs with five rules,
|
|
179
|
+
around 33,000 calls/s. The write buffer batches lines, and the p99 stays under
|
|
180
|
+
5 ms so a flush cannot stall the loop.
|