expr_tracker 0.1.4__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.4
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,5 +1,7 @@
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
6
 
5
7
  _tracker: ContextVar["Tracker | None"] = ContextVar("tracker", default=None)
@@ -12,6 +14,10 @@ def get_backend(backend: str | object):
12
14
  try:
13
15
  import wandb
14
16
 
17
+ wandb.login(
18
+ key=os.getenv("WANDB_API_KEY", None),
19
+ host=os.getenv("WANDB_HOST", None),
20
+ )
15
21
  return wandb
16
22
  except ImportError:
17
23
  raise ImportError("WandB backend is not installed.")
@@ -129,16 +135,22 @@ def log(metrics: dict, step: int | None = None):
129
135
  tracker = _tracker.get()
130
136
  if tracker is None:
131
137
  raise RuntimeError("Tracker is not initialized. Call init() first.")
132
- for b in tracker.backend.values():
133
- 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}")
134
143
 
135
144
 
136
145
  def finish():
137
146
  tracker = _tracker.get()
138
147
  if tracker is None:
139
148
  raise RuntimeError("Tracker is not initialized. Call init() first.")
140
- for b in tracker.backend.values():
141
- 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}")
142
154
  _tracker.set(None)
143
155
 
144
156
 
@@ -147,9 +159,9 @@ def info():
147
159
  if tracker is None:
148
160
  raise RuntimeError("Tracker is not initialized. Call init() first.")
149
161
  info = {}
150
- for b, t in tracker.backend.items():
151
- if b == "wandb":
162
+ for backend, t in tracker.backend.items():
163
+ if backend == "wandb":
152
164
  info["wandb"] = {"url": t.run.url}
153
- if b == "jsonl":
165
+ if backend == "jsonl":
154
166
  info["jsonl"] = {"log_dir": t.log_dir.as_posix()}
155
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