halib 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

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 (39) hide show
  1. halib/__init__.py +3 -3
  2. halib/common/__init__.py +0 -0
  3. halib/common/common.py +178 -0
  4. halib/common/rich_color.py +285 -0
  5. halib/filetype/csvfile.py +3 -9
  6. halib/filetype/ipynb.py +3 -5
  7. halib/filetype/jsonfile.py +0 -3
  8. halib/filetype/textfile.py +0 -1
  9. halib/filetype/videofile.py +91 -2
  10. halib/filetype/yamlfile.py +3 -3
  11. halib/online/projectmake.py +7 -6
  12. halib/online/tele_noti.py +165 -0
  13. halib/research/core/__init__.py +0 -0
  14. halib/research/core/base_config.py +144 -0
  15. halib/research/core/base_exp.py +157 -0
  16. halib/research/core/param_gen.py +108 -0
  17. halib/research/core/wandb_op.py +117 -0
  18. halib/research/data/__init__.py +0 -0
  19. halib/research/data/dataclass_util.py +41 -0
  20. halib/research/data/dataset.py +208 -0
  21. halib/research/data/torchloader.py +165 -0
  22. halib/research/perf/__init__.py +0 -0
  23. halib/research/perf/flop_calc.py +190 -0
  24. halib/research/perf/gpu_mon.py +58 -0
  25. halib/research/perf/perfcalc.py +363 -0
  26. halib/research/perf/perfmetrics.py +137 -0
  27. halib/research/perf/perftb.py +778 -0
  28. halib/research/perf/profiler.py +301 -0
  29. halib/research/viz/__init__.py +0 -0
  30. halib/research/viz/plot.py +754 -0
  31. halib/system/filesys.py +60 -20
  32. halib/system/path.py +73 -0
  33. halib/utils/dict.py +9 -0
  34. halib/utils/list.py +12 -0
  35. {halib-0.2.1.dist-info → halib-0.2.2.dist-info}/METADATA +4 -1
  36. {halib-0.2.1.dist-info → halib-0.2.2.dist-info}/RECORD +39 -14
  37. {halib-0.2.1.dist-info → halib-0.2.2.dist-info}/WHEEL +0 -0
  38. {halib-0.2.1.dist-info → halib-0.2.2.dist-info}/licenses/LICENSE.txt +0 -0
  39. {halib-0.2.1.dist-info → halib-0.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,301 @@
1
+ import os
2
+ import time
3
+ import json
4
+
5
+ from pathlib import Path
6
+ from pprint import pprint
7
+ from threading import Lock
8
+ from loguru import logger
9
+
10
+ from plotly.subplots import make_subplots
11
+ import plotly.graph_objects as go
12
+ import plotly.express as px # for dynamic color scales
13
+
14
+ from ...common.common import ConsoleLog
15
+
16
+
17
+ class zProfiler:
18
+ """A singleton profiler to measure execution time of contexts and steps.
19
+
20
+ Args:
21
+ interval_report (int): Frequency of periodic reports (0 to disable).
22
+ stop_to_view (bool): Pause execution to view reports if True (only in debug mode).
23
+ output_file (str): Path to save the profiling report.
24
+ report_format (str): Output format for reports ("json" or "csv").
25
+
26
+ Example:
27
+ prof = zProfiler()
28
+ prof.ctx_start("my_context")
29
+ prof.step_start("my_context", "step1")
30
+ time.sleep(0.1)
31
+ prof.step_end("my_context", "step1")
32
+ prof.ctx_end("my_context")
33
+ """
34
+
35
+ _instance = None
36
+ _lock = Lock()
37
+
38
+ def __new__(cls, *args, **kwargs):
39
+ with cls._lock:
40
+ if cls._instance is None:
41
+ cls._instance = super().__new__(cls)
42
+ return cls._instance
43
+
44
+ def __init__(
45
+ self,
46
+ ):
47
+ if not hasattr(self, "_initialized"):
48
+ self.time_dict = {}
49
+ self._initialized = True
50
+
51
+ def ctx_start(self, ctx_name="ctx_default"):
52
+ if not isinstance(ctx_name, str) or not ctx_name:
53
+ raise ValueError("ctx_name must be a non-empty string")
54
+ if ctx_name not in self.time_dict:
55
+ self.time_dict[ctx_name] = {
56
+ "start": time.perf_counter(),
57
+ "step_dict": {},
58
+ "report_count": 0,
59
+ }
60
+ self.time_dict[ctx_name]["report_count"] += 1
61
+
62
+ def ctx_end(self, ctx_name="ctx_default", report_func=None):
63
+ if ctx_name not in self.time_dict:
64
+ return
65
+ self.time_dict[ctx_name]["end"] = time.perf_counter()
66
+ self.time_dict[ctx_name]["duration"] = (
67
+ self.time_dict[ctx_name]["end"] - self.time_dict[ctx_name]["start"]
68
+ )
69
+
70
+ def step_start(self, ctx_name, step_name):
71
+ if not isinstance(step_name, str) or not step_name:
72
+ raise ValueError("step_name must be a non-empty string")
73
+ if ctx_name not in self.time_dict:
74
+ return
75
+ if step_name not in self.time_dict[ctx_name]["step_dict"]:
76
+ self.time_dict[ctx_name]["step_dict"][step_name] = []
77
+ self.time_dict[ctx_name]["step_dict"][step_name].append([time.perf_counter()])
78
+
79
+ def step_end(self, ctx_name, step_name):
80
+ if (
81
+ ctx_name not in self.time_dict
82
+ or step_name not in self.time_dict[ctx_name]["step_dict"]
83
+ ):
84
+ return
85
+ self.time_dict[ctx_name]["step_dict"][step_name][-1].append(time.perf_counter())
86
+
87
+ def _step_dict_to_detail(self, ctx_step_dict):
88
+ """
89
+ 'ctx_step_dict': {
90
+ │ │ 'preprocess': [
91
+ │ │ │ [278090.947465806, 278090.960484853],
92
+ │ │ │ [278091.178424035, 278091.230944486],
93
+ │ │ 'infer': [
94
+ │ │ │ [278090.960490534, 278091.178424035],
95
+ │ │ │ [278091.230944486, 278091.251378469],
96
+ │ }
97
+ """
98
+ assert (
99
+ len(ctx_step_dict.keys()) > 0
100
+ ), "step_dict must have only one key (step_name) for detail."
101
+ normed_ctx_step_dict = {}
102
+ for step_name, time_list in ctx_step_dict.items():
103
+ if not isinstance(ctx_step_dict[step_name], list):
104
+ raise ValueError(f"Step data for {step_name} must be a list")
105
+ # step_name = list(ctx_step_dict.keys())[0] # ! debug
106
+ normed_time_ls = []
107
+ for idx, time_data in enumerate(time_list):
108
+ elapsed_time = -1
109
+ if len(time_data) == 2:
110
+ start, end = time_data[0], time_data[1]
111
+ elapsed_time = end - start
112
+ normed_time_ls.append((idx, elapsed_time)) # including step
113
+ normed_ctx_step_dict[step_name] = normed_time_ls
114
+ return normed_ctx_step_dict
115
+
116
+ def get_report_dict(self, with_detail=False):
117
+ report_dict = {}
118
+ for ctx_name, ctx_dict in self.time_dict.items():
119
+ report_dict[ctx_name] = {
120
+ "duration": ctx_dict.get("duration", 0.0),
121
+ "step_dict": {
122
+ "summary": {"avg_time": {}, "percent_time": {}},
123
+ "detail": {},
124
+ },
125
+ }
126
+
127
+ if with_detail:
128
+ report_dict[ctx_name]["step_dict"]["detail"] = (
129
+ self._step_dict_to_detail(ctx_dict["step_dict"])
130
+ )
131
+ avg_time_list = []
132
+ epsilon = 1e-5
133
+ for step_name, step_list in ctx_dict["step_dict"].items():
134
+ durations = []
135
+ try:
136
+ for time_data in step_list:
137
+ if len(time_data) != 2:
138
+ continue
139
+ start, end = time_data
140
+ durations.append(end - start)
141
+ except Exception as e:
142
+ logger.error(
143
+ f"Error processing step {step_name} in context {ctx_name}: {e}"
144
+ )
145
+ continue
146
+ if not durations:
147
+ continue
148
+ avg_time = sum(durations) / len(durations)
149
+ if avg_time < epsilon:
150
+ continue
151
+ avg_time_list.append((step_name, avg_time))
152
+ total_avg_time = (
153
+ sum(time for _, time in avg_time_list) or 1e-10
154
+ ) # Avoid division by zero
155
+ for step_name, avg_time in avg_time_list:
156
+ report_dict[ctx_name]["step_dict"]["summary"]["percent_time"][
157
+ f"per_{step_name}"
158
+ ] = (avg_time / total_avg_time) * 100.0
159
+ report_dict[ctx_name]["step_dict"]["summary"]["avg_time"][
160
+ f"avg_{step_name}"
161
+ ] = avg_time
162
+ report_dict[ctx_name]["step_dict"]["summary"][
163
+ "total_avg_time"
164
+ ] = total_avg_time
165
+ report_dict[ctx_name]["step_dict"]["summary"] = dict(
166
+ sorted(report_dict[ctx_name]["step_dict"]["summary"].items())
167
+ )
168
+ return report_dict
169
+
170
+ @classmethod
171
+ @classmethod
172
+ def plot_formatted_data(
173
+ cls, profiler_data, outdir=None, file_format="png", do_show=False, tag=""
174
+ ):
175
+ """
176
+ Plot each context in a separate figure with bar + pie charts.
177
+ Save each figure in the specified format (png or svg).
178
+ """
179
+
180
+ if outdir is not None:
181
+ os.makedirs(outdir, exist_ok=True)
182
+
183
+ if file_format.lower() not in ["png", "svg"]:
184
+ raise ValueError("file_format must be 'png' or 'svg'")
185
+
186
+ results = {} # {context: fig}
187
+
188
+ for ctx, ctx_data in profiler_data.items():
189
+ summary = ctx_data["step_dict"]["summary"]
190
+ avg_times = summary["avg_time"]
191
+ percent_times = summary["percent_time"]
192
+
193
+ step_names = [s.replace("avg_", "") for s in avg_times.keys()]
194
+ # pprint(f'{step_names=}')
195
+ n_steps = len(step_names)
196
+
197
+ assert n_steps > 0, "No steps found for context: {}".format(ctx)
198
+ # Generate dynamic colors
199
+ colors = px.colors.sample_colorscale(
200
+ "Viridis", [i / (n_steps - 1) for i in range(n_steps)]
201
+ ) if n_steps > 1 else [px.colors.sample_colorscale("Viridis", [0])[0]]
202
+ # pprint(f'{len(colors)} colors generated for {n_steps} steps')
203
+ color_map = dict(zip(step_names, colors))
204
+
205
+ # Create figure
206
+ fig = make_subplots(
207
+ rows=1,
208
+ cols=2,
209
+ subplot_titles=[f"Avg Time", f"% Time"],
210
+ specs=[[{"type": "bar"}, {"type": "pie"}]],
211
+ )
212
+
213
+ # Bar chart
214
+ fig.add_trace(
215
+ go.Bar(
216
+ x=step_names,
217
+ y=list(avg_times.values()),
218
+ text=[f"{v*1000:.2f} ms" for v in avg_times.values()],
219
+ textposition="outside",
220
+ marker=dict(color=[color_map[s] for s in step_names]),
221
+ name="", # unified legend
222
+ showlegend=False,
223
+ ),
224
+ row=1,
225
+ col=1,
226
+ )
227
+
228
+ # Pie chart (colors match bar)
229
+ fig.add_trace(
230
+ go.Pie(
231
+ labels=step_names,
232
+ values=list(percent_times.values()),
233
+ marker=dict(colors=[color_map[s] for s in step_names]),
234
+ hole=0.4,
235
+ name="",
236
+ showlegend=True,
237
+ ),
238
+ row=1,
239
+ col=2,
240
+ )
241
+ tag_str = tag if tag and len(tag) > 0 else ""
242
+ # Layout
243
+ fig.update_layout(
244
+ title_text=f"[{tag_str}] Context Profiler: {ctx}",
245
+ width=1000,
246
+ height=400,
247
+ showlegend=True,
248
+ legend=dict(title="Steps", x=1.05, y=0.5, traceorder="normal"),
249
+ hovermode="x unified",
250
+ )
251
+
252
+ fig.update_xaxes(title_text="Steps", row=1, col=1)
253
+ fig.update_yaxes(title_text="Avg Time (ms)", row=1, col=1)
254
+
255
+ # Show figure
256
+ if do_show:
257
+ fig.show()
258
+
259
+ # Save figure
260
+ if outdir is not None:
261
+ file_prefix = ctx if len(tag_str) == 0 else f"{tag_str}_{ctx}"
262
+ file_path = os.path.join(outdir, f"{file_prefix}_summary.{file_format.lower()}")
263
+ fig.write_image(file_path)
264
+ print(f"Saved figure: {file_path}")
265
+
266
+ results[ctx] = fig
267
+
268
+ return results
269
+
270
+ def report_and_plot(self, outdir=None, file_format="png", do_show=False, tag=""):
271
+ """
272
+ Generate the profiling report and plot the formatted data.
273
+
274
+ Args:
275
+ outdir (str): Directory to save figures. If None, figures are only shown.
276
+ file_format (str): Target file format, "png" or "svg". Default is "png".
277
+ do_show (bool): Whether to display the plots. Default is False.
278
+ """
279
+ report = self.get_report_dict()
280
+ self.get_report_dict(with_detail=False)
281
+ return self.plot_formatted_data(
282
+ report, outdir=outdir, file_format=file_format, do_show=do_show, tag=tag
283
+ )
284
+ def meta_info(self):
285
+ """
286
+ Print the structure of the profiler's time dictionary.
287
+ Useful for debugging and understanding the profiler's internal state.
288
+ """
289
+ for ctx_name, ctx_dict in self.time_dict.items():
290
+ with ConsoleLog(f"Context: {ctx_name}"):
291
+ step_names = list(ctx_dict['step_dict'].keys())
292
+ for step_name in step_names:
293
+ pprint(f"Step: {step_name}")
294
+
295
+ def save_report_dict(self, output_file, with_detail=False):
296
+ try:
297
+ report = self.get_report_dict(with_detail=with_detail)
298
+ with open(output_file, "w") as f:
299
+ json.dump(report, f, indent=4)
300
+ except Exception as e:
301
+ logger.error(f"Failed to save report to {output_file}: {e}")
File without changes