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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: expr_tracker
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Add your description here
5
5
  Author-email: HSPK <whxway@whu.edu.cn>
6
6
  Requires-Python: >=3.10
@@ -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
- from typing import Literal
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 b in tracker.backend.values():
138
- b.log(metrics, step=step)
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 b in tracker.backend.values():
146
- b.finish()
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 b, t in tracker.backend.items():
156
- if b == "wandb":
162
+ for backend, t in tracker.backend.items():
163
+ if backend == "wandb":
157
164
  info["wandb"] = {"url": t.run.url}
158
- if b == "jsonl":
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