expr_tracker 0.1.5__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/PKG-INFO +1 -1
- expr_tracker-0.1.6/src/expr_tracker/jsonl.py +107 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/tracker.py +16 -9
- expr_tracker-0.1.5/src/expr_tracker/jsonl.py +0 -59
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/.gitignore +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/README.md +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/pyproject.toml +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/__init__.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/_compat.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/alert.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/cli.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/encoders.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/src/expr_tracker/types.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/tests/test_tracker.py +0 -0
- {expr_tracker-0.1.5 → expr_tracker-0.1.6}/uv.lock +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import jsonlines
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from expr_tracker.encoders import jsonable_encoder
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonlTracker:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.buffer = []
|
|
14
|
+
self.buffer_size = 50
|
|
15
|
+
self.log_fp = None
|
|
16
|
+
|
|
17
|
+
def init(
|
|
18
|
+
self,
|
|
19
|
+
project: str,
|
|
20
|
+
name: str | None = None,
|
|
21
|
+
config: dict | None = None,
|
|
22
|
+
dir: str | None = None,
|
|
23
|
+
print_to_screen: bool = False,
|
|
24
|
+
print_handle=print,
|
|
25
|
+
buffer_size: int = 50, # 新增 buffer_size 参数
|
|
26
|
+
**kwargs,
|
|
27
|
+
):
|
|
28
|
+
self.project = project
|
|
29
|
+
self.name = name
|
|
30
|
+
if dir is None:
|
|
31
|
+
dir = "./tracker/jsonl"
|
|
32
|
+
self.log_dir = Path(dir) / self.project / self.name
|
|
33
|
+
self.config_fp = self.log_dir / "config.json"
|
|
34
|
+
self.log_fp = self.log_dir / "metrics.jsonl"
|
|
35
|
+
|
|
36
|
+
# 初始化 Buffer 配置
|
|
37
|
+
self.buffer = []
|
|
38
|
+
self.buffer_size = buffer_size
|
|
39
|
+
|
|
40
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
if self.config_fp.exists():
|
|
43
|
+
logger.warning(
|
|
44
|
+
f"Config file {self.config_fp} already exists. It will be overwritten."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if config is not None:
|
|
48
|
+
# Config 通常只写一次,直接写入即可
|
|
49
|
+
with open(self.config_fp, "w") as f:
|
|
50
|
+
json.dump(jsonable_encoder(config), f, indent=4)
|
|
51
|
+
|
|
52
|
+
self.print_to_screen = print_to_screen
|
|
53
|
+
self.print_handle = print_handle
|
|
54
|
+
|
|
55
|
+
# 优化:流式计算行数,避免一次性加载大文件到内存 (对 BlobFuse 友好)
|
|
56
|
+
self.current_step = 0
|
|
57
|
+
if self.log_fp.exists():
|
|
58
|
+
try:
|
|
59
|
+
with open(self.log_fp, "rb") as f:
|
|
60
|
+
self.current_step = sum(1 for _ in f)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.warning(f"Could not count existing lines in {self.log_fp}: {e}")
|
|
63
|
+
|
|
64
|
+
# 注册退出钩子:确保程序意外终止时也能写入剩余数据
|
|
65
|
+
atexit.register(self.flush)
|
|
66
|
+
|
|
67
|
+
def log(self, metrics: dict, step: int | None = None):
|
|
68
|
+
if step is not None:
|
|
69
|
+
self.current_step = step
|
|
70
|
+
|
|
71
|
+
record = {"_step": self.current_step, **metrics}
|
|
72
|
+
|
|
73
|
+
# 1. 写入内存 Buffer
|
|
74
|
+
self.buffer.append(record)
|
|
75
|
+
|
|
76
|
+
# 2. 屏幕打印
|
|
77
|
+
if self.print_to_screen:
|
|
78
|
+
self.print_handle(f"{record}")
|
|
79
|
+
|
|
80
|
+
# 3. 检查 Buffer 是否已满
|
|
81
|
+
if len(self.buffer) >= self.buffer_size:
|
|
82
|
+
self.flush()
|
|
83
|
+
|
|
84
|
+
self.current_step += 1
|
|
85
|
+
|
|
86
|
+
def flush(self):
|
|
87
|
+
"""强制将内存中的 Buffer 写入磁盘"""
|
|
88
|
+
if not self.buffer:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# 确保目录存在 (防止运行时目录被删)
|
|
92
|
+
if self.log_fp and not self.log_fp.parent.exists():
|
|
93
|
+
self.log_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# 批量追加写入
|
|
97
|
+
with jsonlines.open(self.log_fp, mode="a") as writer:
|
|
98
|
+
writer.write_all(self.buffer)
|
|
99
|
+
self.buffer = [] # 写入成功后清空 Buffer
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Failed to flush metrics to {self.log_fp}: {e}")
|
|
102
|
+
|
|
103
|
+
def finish(self):
|
|
104
|
+
"""结束时显式调用"""
|
|
105
|
+
self.flush()
|
|
106
|
+
# 如果手动调用了 finish,取消 atexit 注册,防止重复调用
|
|
107
|
+
atexit.unregister(self.flush)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
2
|
from contextvars import ContextVar
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
3
5
|
from loguru import logger
|
|
4
|
-
import os
|
|
5
6
|
|
|
6
7
|
_tracker: ContextVar["Tracker | None"] = ContextVar("tracker", default=None)
|
|
7
8
|
|
|
@@ -134,16 +135,22 @@ def log(metrics: dict, step: int | None = None):
|
|
|
134
135
|
tracker = _tracker.get()
|
|
135
136
|
if tracker is None:
|
|
136
137
|
raise RuntimeError("Tracker is not initialized. Call init() first.")
|
|
137
|
-
for
|
|
138
|
-
|
|
138
|
+
for backend, t in tracker.backend.items():
|
|
139
|
+
try:
|
|
140
|
+
t.log(metrics, step=step)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Failed to log metrics to {backend}: {e}")
|
|
139
143
|
|
|
140
144
|
|
|
141
145
|
def finish():
|
|
142
146
|
tracker = _tracker.get()
|
|
143
147
|
if tracker is None:
|
|
144
148
|
raise RuntimeError("Tracker is not initialized. Call init() first.")
|
|
145
|
-
for
|
|
146
|
-
|
|
149
|
+
for backend, t in tracker.backend.items():
|
|
150
|
+
try:
|
|
151
|
+
t.finish()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning(f"Failed to finish tracker for {backend}: {e}")
|
|
147
154
|
_tracker.set(None)
|
|
148
155
|
|
|
149
156
|
|
|
@@ -152,9 +159,9 @@ def info():
|
|
|
152
159
|
if tracker is None:
|
|
153
160
|
raise RuntimeError("Tracker is not initialized. Call init() first.")
|
|
154
161
|
info = {}
|
|
155
|
-
for
|
|
156
|
-
if
|
|
162
|
+
for backend, t in tracker.backend.items():
|
|
163
|
+
if backend == "wandb":
|
|
157
164
|
info["wandb"] = {"url": t.run.url}
|
|
158
|
-
if
|
|
165
|
+
if backend == "jsonl":
|
|
159
166
|
info["jsonl"] = {"log_dir": t.log_dir.as_posix()}
|
|
160
167
|
return info
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
from loguru import logger
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from expr_tracker.encoders import jsonable_encoder
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class JsonlTracker:
|
|
7
|
-
def __init__(self):
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
def init(
|
|
11
|
-
self,
|
|
12
|
-
project: str,
|
|
13
|
-
name: str | None = None,
|
|
14
|
-
config: dict | None = None,
|
|
15
|
-
dir: str | None = None,
|
|
16
|
-
print_to_screen: bool = False,
|
|
17
|
-
print_handle=print,
|
|
18
|
-
**kwargs,
|
|
19
|
-
):
|
|
20
|
-
self.project = project
|
|
21
|
-
self.name = name
|
|
22
|
-
if dir is None:
|
|
23
|
-
dir = "./tracker/jsonl"
|
|
24
|
-
self.log_dir = Path(dir) / self.project / self.name
|
|
25
|
-
self.config_fp = self.log_dir / "config.json"
|
|
26
|
-
self.log_fp = self.log_dir / "metrics.jsonl"
|
|
27
|
-
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
-
if self.config_fp.exists():
|
|
29
|
-
logger.warning(
|
|
30
|
-
f"Config file {self.config_fp} already exists. It will be overwritten."
|
|
31
|
-
)
|
|
32
|
-
if config is not None:
|
|
33
|
-
with open(self.config_fp, "w") as f:
|
|
34
|
-
import json
|
|
35
|
-
|
|
36
|
-
json.dump(jsonable_encoder(config), f, indent=4)
|
|
37
|
-
self.print_to_screen = print_to_screen
|
|
38
|
-
self.print_handle = print_handle
|
|
39
|
-
self.current_step = (
|
|
40
|
-
0 if not self.log_fp.exists() else len(self.log_fp.read_text().splitlines())
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
def log(self, metrics: dict, step: int | None = None):
|
|
44
|
-
import jsonlines
|
|
45
|
-
|
|
46
|
-
if not self.log_fp.parent.exists():
|
|
47
|
-
self.log_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
-
|
|
49
|
-
if step is not None:
|
|
50
|
-
self.current_step = step
|
|
51
|
-
metrics = {"_step": self.current_step, **metrics}
|
|
52
|
-
with jsonlines.open(self.log_fp, mode="a") as writer:
|
|
53
|
-
writer.write(metrics)
|
|
54
|
-
self.current_step += 1
|
|
55
|
-
if self.print_to_screen:
|
|
56
|
-
self.print_handle(f"{metrics}")
|
|
57
|
-
|
|
58
|
-
def finish(self):
|
|
59
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|