Skip to content

Commit b4e1d54

Browse files
authored
Merge pull request #72 from Gromwud/main
Perfomance improvements and refactoring
2 parents a3f8700 + e5979c3 commit b4e1d54

26 files changed

Lines changed: 1670 additions & 428 deletions

epde/_loop_stats.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Lightweight retry/condition-loop instrumentation.
2+
3+
Off by default. Set ``EPDE_LOOP_STATS=1`` to enable; ~30 source-level
4+
``record(...)`` call sites then accumulate per-loop stats that
5+
``report()`` formats as a table.
6+
7+
Cost when disabled: a single global-var read per ``record`` call.
8+
Cost when enabled: a dict lookup + list append per loop exit.
9+
"""
10+
from __future__ import annotations
11+
12+
import os
13+
import sys
14+
from collections import defaultdict
15+
from typing import Optional
16+
17+
_ENABLED = os.environ.get('EPDE_LOOP_STATS', '0') == '1'
18+
19+
20+
def _new_bucket():
21+
return {'entries': 0, 'iters': [], 'hit_cap': 0, 'early_exit': 0, 'caps': set()}
22+
23+
24+
_stats = defaultdict(_new_bucket)
25+
26+
27+
def enabled() -> bool:
28+
return _ENABLED
29+
30+
31+
def record(site: str, iters: int, cap: int) -> None:
32+
"""Record one loop exit.
33+
34+
``site`` is a human label like ``"EqRPS.outer"``. ``iters`` is the
35+
number of iterations actually executed. ``cap`` is the loop's
36+
maximum (use ``sys.maxsize`` for condition-driven loops with no
37+
explicit cap).
38+
"""
39+
if not _ENABLED:
40+
return
41+
b = _stats[site]
42+
b['entries'] += 1
43+
b['iters'].append(iters)
44+
b['caps'].add(cap)
45+
if iters >= cap:
46+
b['hit_cap'] += 1
47+
elif iters <= 1:
48+
b['early_exit'] += 1
49+
50+
51+
def reset() -> None:
52+
_stats.clear()
53+
54+
55+
def _stats_for(name: str) -> dict:
56+
b = _stats[name]
57+
n = b['entries']
58+
iters = b['iters']
59+
if n == 0:
60+
return {'entries': 0}
61+
iters_sorted = sorted(iters)
62+
median = iters_sorted[n // 2]
63+
return {
64+
'entries': n,
65+
'mean': sum(iters) / n,
66+
'median': median,
67+
'max': max(iters),
68+
'p95': iters_sorted[min(n - 1, int(n * 0.95))],
69+
'total_iters': sum(iters),
70+
'hit_cap_pct': 100.0 * b['hit_cap'] / n,
71+
'early_exit_pct': 100.0 * b['early_exit'] / n,
72+
'cap': max(b['caps']) if b['caps'] else 0,
73+
}
74+
75+
76+
def report(path: Optional[str] = None) -> str:
77+
"""Format all recorded loops as a table, sorted by total iterations.
78+
79+
Writes to ``path`` if given AND also returns the string.
80+
"""
81+
sites = sorted(_stats.keys(),
82+
key=lambda s: -sum(_stats[s]['iters']) if _stats[s]['iters'] else 0)
83+
lines = []
84+
header = (f"{'site':<45} {'entries':>8} {'mean':>7} {'med':>5} "
85+
f"{'p95':>5} {'max':>5} {'cap':>6} {'%cap':>6} "
86+
f"{'%early':>7} {'totIters':>10}")
87+
lines.append(header)
88+
lines.append('-' * len(header))
89+
if not _ENABLED:
90+
lines.append('(EPDE_LOOP_STATS disabled -- set EPDE_LOOP_STATS=1 to record)')
91+
for site in sites:
92+
s = _stats_for(site)
93+
if s['entries'] == 0:
94+
continue
95+
cap_str = 'inf' if s['cap'] >= sys.maxsize else str(s['cap'])
96+
lines.append(
97+
f"{site:<45} {s['entries']:>8d} {s['mean']:>7.2f} "
98+
f"{s['median']:>5d} {s['p95']:>5d} {s['max']:>5d} "
99+
f"{cap_str:>6} {s['hit_cap_pct']:>5.1f}% "
100+
f"{s['early_exit_pct']:>6.1f}% {s['total_iters']:>10d}"
101+
)
102+
text = '\n'.join(lines)
103+
if path is not None:
104+
with open(path, 'w') as f:
105+
f.write(text + '\n')
106+
return text

epde/evaluators.py

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,19 @@ def __call__(self, factor, structural: bool = False, grids: list = None,
2828

2929

3030
class CustomEvaluator(EvaluatorTemplate):
31-
def __init__(self, evaluation_functions_np: Union[Callable, dict] = None,
31+
def __init__(self, evaluation_functions_np: Union[Callable, dict] = None,
3232
evaluation_functions_torch: Union[Callable, dict] = None,
33-
eval_fun_params_labels: Union[list, tuple, set] = ['power']):
33+
eval_fun_params_labels: Union[list, tuple, set] = ['power'],
34+
native_vectorized: bool = False):
35+
"""Wrap one or many evaluation functions for use as a factor evaluator.
36+
37+
``native_vectorized=True`` skips the per-element ``np.vectorize``
38+
dispatch on the hot path: the func is called ONCE with the full
39+
grid arrays. The built-in evaluators in this module all set this
40+
flag because their numpy ops (``np.cos``, ``np.sin``, ``np.power``,
41+
``np.full_like``, etc.) vectorize natively. User code passing a
42+
non-vectorising callable should leave the default ``False``.
43+
"""
3444
self._evaluation_functions_np = evaluation_functions_np
3545
self._evaluation_functions_torch = evaluation_functions_torch
3646

@@ -43,6 +53,7 @@ def __init__(self, evaluation_functions_np: Union[Callable, dict] = None,
4353
self._single_function_token = True
4454

4555
self.eval_fun_params_labels = eval_fun_params_labels
56+
self.native_vectorized = native_vectorized
4657

4758
def __call__(self, factor, structural: bool = False, func_args: List[Union[torch.Tensor, np.ndarray]] = None,
4859
torch_mode: bool = False, **kwargs): # s
@@ -67,23 +78,32 @@ def __call__(self, factor, structural: bool = False, func_args: List[Union[torch
6778
if param_descr['name'] == key:
6879
eval_fun_kwargs[key] = factor.params[param_idx]
6980

70-
grid_function = np.vectorize(lambda args: funcs(*args, **eval_fun_kwargs))
71-
7281
if func_args is None:
7382
new_grid = False
7483
func_args = factor.grids
7584
else:
7685
new_grid = True
77-
try:
78-
if new_grid:
79-
raise AttributeError
80-
self.indexes_vect
81-
except AttributeError:
82-
self.indexes_vect = np.empty_like(func_args[0], dtype=object)
83-
for tensor_idx, _ in np.ndenumerate(func_args[0]):
84-
self.indexes_vect[tensor_idx] = tuple([subarg[tensor_idx]
85-
for subarg in func_args])
86-
value = grid_function(self.indexes_vect)
86+
87+
if self.native_vectorized:
88+
# Fast path: call funcs once with the full grid arrays. The
89+
# built-in numpy evaluators (trig, sign, grid, inverse,
90+
# const, velocity) all return an array of shape
91+
# ``func_args[0].shape``. This skips an N-element
92+
# ``np.vectorize`` loop that on Wave (65k samples)
93+
# dominated evaluator self-time at ~35 s per run.
94+
value = funcs(*func_args, **eval_fun_kwargs)
95+
else:
96+
grid_function = np.vectorize(lambda args: funcs(*args, **eval_fun_kwargs))
97+
try:
98+
if new_grid:
99+
raise AttributeError
100+
self.indexes_vect
101+
except AttributeError:
102+
self.indexes_vect = np.empty_like(func_args[0], dtype=object)
103+
for tensor_idx, _ in np.ndenumerate(func_args[0]):
104+
self.indexes_vect[tensor_idx] = tuple([subarg[tensor_idx]
105+
for subarg in func_args])
106+
value = grid_function(self.indexes_vect)
87107
value = value[global_var.grid_cache.g_func != 0]
88108
value = value.reshape(-1)
89109
return value
@@ -125,7 +145,9 @@ def simple_function_evaluator(factor, structural: bool = False, grids=None,
125145

126146
else:
127147
if factor.params[power_param_idx] == 1:
128-
value = global_var.tensor_cache.get(factor.cache_label, structural = structural, torch_mode = torch_mode)
148+
# Same bucketed key Factor.evaluate uses so trig factors with
149+
# within-tolerance freq share a single cached evaluation.
150+
value = global_var.tensor_cache.get(factor.structural_label, structural = structural, torch_mode = torch_mode)
129151
return value
130152
else:
131153
value = global_var.tensor_cache.get(factor_params_to_str(factor, set_default_power = True,
@@ -259,30 +281,37 @@ def vhef_grad_15(*grids, **kwargs):
259281
vhef_grad_10, vhef_grad_11, vhef_grad_12,
260282
vhef_grad_13, vhef_grad_14, vhef_grad_15]
261283

262-
sign_evaluator = CustomEvaluator(evaluation_functions_np=sign_eval_fun_np,
263-
evaluation_functions_torch=sign_eval_fun_torch,
264-
eval_fun_params_labels = ['power', 'dim'])
284+
sign_evaluator = CustomEvaluator(evaluation_functions_np=sign_eval_fun_np,
285+
evaluation_functions_torch=sign_eval_fun_torch,
286+
eval_fun_params_labels = ['power', 'dim'],
287+
native_vectorized=True)
265288

266-
phased_sine_evaluator = CustomEvaluator(evaluation_functions_np = phased_sine_1d_np,
289+
phased_sine_evaluator = CustomEvaluator(evaluation_functions_np = phased_sine_1d_np,
267290
evaluation_functions_torch = phased_sine_1d_torch,
268-
eval_fun_params_labels = ['power', 'freq', 'phase']) # , use_factors_grids = True
291+
eval_fun_params_labels = ['power', 'freq', 'phase'],
292+
native_vectorized=True) # , use_factors_grids = True
269293
trigonometric_evaluator = CustomEvaluator(evaluation_functions_np = trig_eval_fun_np,
270294
evaluation_functions_torch = trig_eval_fun_torch,
271-
eval_fun_params_labels=['freq', 'dim', 'power']) # , use_factors_grids = True
295+
eval_fun_params_labels=['freq', 'dim', 'power'],
296+
native_vectorized=True) # , use_factors_grids = True
272297
grid_evaluator = CustomEvaluator(evaluation_functions_np = grid_eval_fun_np,
273298
evaluation_functions_torch = grid_eval_fun_torch,
274-
eval_fun_params_labels=['dim', 'power']) # , use_factors_grids=True
299+
eval_fun_params_labels=['dim', 'power'],
300+
native_vectorized=True) # , use_factors_grids=True
275301

276302
inverse_function_evaluator = CustomEvaluator(evaluation_functions_np = inverse_eval_fun_np,
277303
evaluation_functions_torch = inverse_eval_fun_torch,
278-
eval_fun_params_labels=['dim', 'power']) # , use_factors_grids=True
304+
eval_fun_params_labels=['dim', 'power'],
305+
native_vectorized=True) # , use_factors_grids=True
279306

280307
const_evaluator = CustomEvaluator(evaluation_functions_np = const_eval_fun_np,
281-
evaluation_functions_torch = const_eval_fun_torch,
282-
eval_fun_params_labels = ['power', 'value'])
308+
evaluation_functions_torch = const_eval_fun_torch,
309+
eval_fun_params_labels = ['power', 'value'],
310+
native_vectorized=True)
283311
const_grad_evaluator = CustomEvaluator(evaluation_functions_np = const_grad_fun_np,
284312
evaluation_functions_torch = const_grad_fun_np,
285-
eval_fun_params_labels = ['power', 'value'])
313+
eval_fun_params_labels = ['power', 'value'],
314+
native_vectorized=True)
286315

287316
velocity_evaluator = CustomEvaluator(velocity_heating_eval_fun, ['p' + str(idx+1) for idx in range(15)])
288317
velocity_grad_evaluators = [CustomEvaluator(component, ['p' + str(idx+1) for idx in range(15)])

epde/interface/token_family.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,8 @@ def evaluate_all(self, all_vars: List[str]):
396396
generated_token.use_grids_cache()
397397
generated_token.scaled = False
398398
_ = generated_token.evaluate()
399-
print(generated_token.cache_label)
400-
if generated_token.cache_label not in global_var.tensor_cache.memory_default['numpy'].keys():
399+
print(generated_token.structural_label)
400+
if generated_token.structural_label not in global_var.tensor_cache.memory_default['numpy'].keys():
401401
raise KeyError('Generated token somehow was not stored in cache.')
402402

403403

epde/operators/common/fitness.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
186186

187187
fitness_value = rl_error
188188

189-
# if force_out_of_place:
190-
# return fitness_value
189+
if force_out_of_place:
190+
return fitness_value
191191

192192
objective.aic = None
193193
objective.aic_calculated = True
@@ -211,8 +211,8 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
211211
cv = (std ** 2) / (mu ** 2)
212212
total_lr = sum(cv) / len(data_shape)
213213

214-
if force_out_of_place:
215-
return fitness_value * total_lr
214+
# if force_out_of_place:
215+
# return fitness_value * total_lr
216216

217217
objective.fitness_calculated = True
218218
objective.fitness_value = fitness_value

0 commit comments

Comments
 (0)