malkhut(perf): parallel episode eval — 9x single-eval speedup, zero fidelity loss

- parallel_eval.py: ProcessPoolExecutor-based episode runner. Each worker
  gets its own CWM + planner instance. Zero shared state = embarrassingly
  parallel. Deterministic: same seed → same result.
- PolicyEvaluator.evaluate_candidate: new workers parameter (0=sequential,
  >1=parallel). Backward compatible: default workers=0.
- 16 new tests: determinism, pickling, result validity, cross-validation
  between sequential and parallel paths, backward compatibility.
- README: training performance table with speedup measurements.

Speedup results (3 assets × 30 scenarios = 90 scenarios):
  Sequential:  3.3s per eval  (1.0x)
  2 workers:   1.3s per eval  (2.6x)
  4 workers:   0.6s per eval  (5.9x)
  8 workers:   0.4s per eval  (9.1x)
  CMA-ES 48 evals: 125s → 85s (1.5x training speedup)

Note: CWM numba hot path was already wired (_HAS_NUMBA=True, 5.3µs/transition).
Bottleneck is MCTS planner (96% of eval time), not CWM.
This commit is contained in:
Codex
2026-07-11 20:19:32 +02:00
parent 257c48b127
commit be0e1468da
4 changed files with 371 additions and 5 deletions

View File

@@ -963,11 +963,17 @@ class PolicyEvaluator:
planner_type: str = "sm_mcts",
record_to_matrix: bool = False,
matrix: Optional[Any] = None,
workers: int = 0,
) -> Tuple[float, list[EpisodeResult]]:
results: list[EpisodeResult] = []
for scenario in scenarios:
result = self._run_episode(params, scenario, rng_seed, planner_type)
results.append(result)
if workers > 1 and len(scenarios) > 1:
from malkhut.training.parallel_eval import ParallelEpisodeRunner
runner = ParallelEpisodeRunner(workers=workers)
results = runner.run_episodes(params, scenarios, rng_seed, planner_type)
else:
results = []
for scenario in scenarios:
result = self._run_episode(params, scenario, rng_seed, planner_type)
results.append(result)
# TIE-IN: Record strategy × regime performance
if record_to_matrix and matrix is not None:
tags = scenario.tags