Files
ProStock/src/factors/engine/compute_engine.py
liaozhaorun 181994f063 perf(factors/engine): 重构计算引擎使用 Polars 原生并行
- 移除 Python 多进程/多线程池,消除 DataFrame 序列化开销
- 采用 BFS 分层执行策略,每层表达式通过单次 with_columns 提交
- 利用 Polars Rust 引擎实现零拷贝并行计算
- 添加死锁检测机制处理依赖环
2026-03-14 01:24:52 +08:00

157 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""计算引擎。
执行并行运算,负责将执行计划应用到数据上。
利用 Polars 底层 Rust 引擎的原生并行能力,通过 BFS 分层执行策略
避免 Python 层面的多进程/多线程开销。
"""
from typing import Dict, List, Set
import polars as pl
from src.factors.engine.data_spec import ExecutionPlan
class ComputeEngine:
"""计算引擎 - 执行并行运算。
负责将执行计划应用到数据上,利用 Polars 底层 Rust 引擎的原生并行能力。
采用 BFS 分层执行策略:
1. 构建依赖图,识别各计划间的依赖关系
2. 按拓扑排序分层,每层包含互不依赖的计划
3. 将每层计划打包为表达式列表,通过单次 with_columns 提交
4. Polars 自动在所有 CPU 核心上并行计算,零拷贝内存
"""
def __init__(self) -> None:
"""初始化计算引擎。"""
pass
def execute(
self,
plan: ExecutionPlan,
data: pl.DataFrame,
) -> pl.DataFrame:
"""执行单个计算计划。
Args:
plan: 执行计划
data: 输入数据(核心宽表)
Returns:
包含因子结果的 DataFrame
"""
# 检查依赖字段是否存在
missing_cols = plan.dependencies - set(data.columns)
if missing_cols:
raise ValueError(f"数据缺少必要的字段: {missing_cols}")
# 执行计算
return data.with_columns([plan.polars_expr.alias(plan.output_name)])
def execute_batch(
self,
plans: List[ExecutionPlan],
data: pl.DataFrame,
) -> pl.DataFrame:
"""顺序批量执行多个计算计划。
Args:
plans: 执行计划列表
data: 输入数据
Returns:
包含所有因子结果的 DataFrame
"""
result = data
for plan in plans:
result = self.execute(plan, result)
return result
def execute_parallel(
self,
plans: List[ExecutionPlan],
data: pl.DataFrame,
) -> pl.DataFrame:
"""分层并行执行计算计划(利用 Polars 原生并发优化)。
抛弃 Python 的多进程/多线程池采用计算图拓扑分层BFS DAG
将每一层互不依赖的表达式列表打包,通过单次 with_columns 交给 Polars
由底层 Rust 引擎自动调度并行计算,实现零拷贝性能最大化。
Args:
plans: 执行计划列表
data: 输入数据
Returns:
包含所有因子结果的 DataFrame
Raises:
RuntimeError: 当存在依赖环或缺少基础依赖字段时
"""
if not plans:
return data
result = data
available_cols: Set[str] = set(result.columns)
# 复制一份计划列表用于迭代
remaining_plans = plans.copy()
while remaining_plans:
# 找出当前可以执行的所有独立计划(即依赖的所有列都已就绪)
current_layer: List[ExecutionPlan] = []
next_remaining: List[ExecutionPlan] = []
for plan in remaining_plans:
if plan.dependencies <= available_cols:
current_layer.append(plan)
else:
next_remaining.append(plan)
# 安全兜底:如果一轮遍历后没找到任何可执行计划,说明存在依赖环或数据缺失
if not current_layer:
missing = remaining_plans[0].dependencies - available_cols
raise RuntimeError(
f"计算发生死锁或缺少基础依赖字段!\n"
f"因子 '{remaining_plans[0].output_name}' 缺少: {missing}"
)
# 核心优化:利用 Polars 内部 Rust 级多线程引擎执行当前层
exprs = [plan.polars_expr.alias(plan.output_name) for plan in current_layer]
result = result.with_columns(exprs)
# 更新已就绪字段集合,为计算下一层做准备
for plan in current_layer:
available_cols.add(plan.output_name)
remaining_plans = next_remaining
return result
def compute(
self,
plans: List[ExecutionPlan],
data: pl.DataFrame,
parallel: bool = True,
) -> pl.DataFrame:
"""智能计算入口。
根据 parallel 参数自动选择执行模式:
- True: 使用分层并行执行(推荐)
- False: 使用顺序执行
Args:
plans: 执行计划列表
data: 输入数据
parallel: 是否使用并行执行
Returns:
包含所有因子结果的 DataFrame
"""
if parallel:
return self.execute_parallel(plans, data)
return self.execute_batch(plans, data)