Files
sentiment-engine/MALKHUT/malkhut/training/parallel.py

135 lines
4.0 KiB
Python
Raw Normal View History

"""
Training Parallelism — parallel evaluation across scenarios.
CMA-ES evaluates sequentially. This module parallelizes evaluation
across scenarios for 4-8x faster convergence.
"""
from __future__ import annotations
import concurrent.futures
import time
from dataclasses import dataclass
from typing import Any, Callable, List, Optional, Sequence
from malkhut.state import FulfilmentPolicyParams, MarketWorldState
from malkhut.training.cma_trainer import PolicyEvaluator, Scenario
class ParallelEvaluator:
"""
Parallel evaluation of strategies across scenarios.
Uses ThreadPoolExecutor for I/O-bound scenarios.
Uses ProcessPoolExecutor for CPU-bound scenarios.
"""
def __init__(
self,
evaluator: PolicyEvaluator,
max_workers: int = 4,
) -> None:
self.evaluator = evaluator
self._max_workers = max_workers
def evaluate_candidate(
self,
params: FulfilmentPolicyParams,
scenarios: Sequence[Scenario],
rng_seed: int = 0,
) -> tuple[float, list]:
"""Evaluate candidate with parallel scenario execution."""
if len(scenarios) <= 1 or self._max_workers <= 1:
return self.evaluator.evaluate_candidate(params, scenarios, rng_seed)
# Split scenarios across workers
chunk_size = max(1, len(scenarios) // self._max_workers)
chunks = []
for i in range(0, len(scenarios), chunk_size):
chunks.append(scenarios[i:i + chunk_size])
# Parallel evaluation
all_results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=self._max_workers) as executor:
futures = []
for i, chunk in enumerate(chunks):
future = executor.submit(
self.evaluator.evaluate_candidate,
params, chunk, rng_seed + i,
)
futures.append(future)
for future in concurrent.futures.as_completed(futures):
score, results = future.result()
all_results.extend(results)
# Aggregate scores
if all_results:
scores = [r.pnl_bps for r in all_results]
avg_score = sum(scores) / len(scores)
else:
avg_score = 0.0
return avg_score, all_results
@dataclass(frozen=True, slots=True)
class TrainingMetrics:
"""Metrics for a training run."""
total_time_s: float
generations: int
total_evals: int
best_score: float
avg_score: float
score_improvement: float
convergence_gen: int
class TrainingMonitor:
"""
Monitor training progress and convergence.
Tracks metrics, detects convergence, logs progress.
"""
def __init__(self) -> None:
self._scores: list[float] = []
self._times: list[float] = []
self._start_time = time.time()
def record_generation(self, score: float) -> None:
self._scores.append(score)
self._times.append(time.time())
@property
def best_score(self) -> float:
return max(self._scores) if self._scores else 0.0
@property
def avg_score(self) -> float:
return sum(self._scores) / len(self._scores) if self._scores else 0.0
@property
def improvement(self) -> float:
if len(self._scores) < 2:
return 0.0
return self._scores[-1] - self._scores[0]
@property
def converged(self) -> bool:
if len(self._scores) < 5:
return False
recent = self._scores[-5:]
variance = sum((s - self.avg_score) ** 2 for s in recent) / len(recent)
return variance < 0.01 # low variance = converged
def metrics(self) -> TrainingMetrics:
return TrainingMetrics(
total_time_s=time.time() - self._start_time,
generations=len(self._scores),
total_evals=0,
best_score=self.best_score,
avg_score=self.avg_score,
score_improvement=self.improvement,
convergence_gen=len(self._scores) if self.converged else -1,
)