expr-tracker 0.2.4__tar.gz → 0.2.6__tar.gz

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