评测并发执行架构:评测任务并行化要注意数据隔离
评测并发执行架构评测任务并行化要注意数据隔离一、见证奇迹的时刻200 个评测任务并行跑结果却对不上号在模型评测场景中经常需要同时评估模型在多个 Benchmark 上的表现。MMLU 有 57 个子任务每个子任务包含数百道题。如果串行跑等所有结果出来可能已经是几小时后。自然的想法是并行化把不同子任务分给不同的进程/线程同时跑。但问题随之而来同样的模型、同样的数据、同样的代码并行执行的结果与串行执行不一致。排查后发现是数据隔离出了问题——不同的子任务共享了全局状态如随机数种子、文件句柄、模型实例的 KV Cache导致结果交叉污染。评测对结果一致性的要求比一般推理服务高得多。一个 0.1% 的准确率偏差可能直接影响两个模型的相对排名。而并行化引入的不确定性——race condition、全局状态交叉、缓存冲突——恰好是评测中最难排查的错误源。二、共享状态与隔离边界为什么并行评测不是简单的多进程flowchart TD A[评测任务调度器] -- B{隔离策略选择} B -- C[进程级隔离] B -- D[容器级隔离] B -- E[显存分区隔离] C -- C1[独立 Python 进程] C1 -- C2[独立模型实例加载] C2 -- C3{优缺点} C3 -- C4[优点: 完全隔离] C3 -- C5[缺点: 显存冗余大] D -- D1[Docker 容器] D1 -- D2[独立文件系统与网络] D2 -- D3{优缺点} D3 -- D4[优点: 环境完全隔离] D3 -- D5[缺点: 启动开销高] E -- E1[CUDA MPS 或 MIG] E1 -- E2[GPU 资源分区] E2 -- E3{优缺点} E3 -- E4[优点: 显存利用率高] E3 -- E5[缺点: 需要硬件支持] F[隔离检查清单] F -- F1[随机数种子独立] F -- F2[模型权重只读共享] F -- F3[数据集按分片读取] F -- F4[输出目录不重叠] F -- F5[日志不交叉写入] style C4 fill:#2e7d32,color:#fff style C5 fill:#c62828,color:#fff style D4 fill:#2e7d32,color:#fff style D5 fill:#c62828,color:#fff style E4 fill:#2e7d32,color:#fff style E5 fill:#c62828,color:#fff核心隔离维度随机数状态隔离每个子任务需要独立的随机数种子避免 data shuffle 和 dropout 的结果交叉模型状态隔离多个子任务不能共享同一个模型实例的推理状态KV Cache需要独立的推理上下文文件系统隔离输出目录、日志文件、临时文件不能重叠避免写入冲突GPU 显存隔离多进程共享 GPU 时需要确保显存分配不冲突。三、多进程评测架构用 Worker Pool 实现数据与状态隔离import multiprocessing as mp from dataclasses import dataclass from typing import List, Dict import torch import numpy as np dataclass class EvalTask: 单个评测任务定义 task_id: str dataset_path: str model_path: str batch_size: int 32 seed: int 42 # 独立随机数种子 def eval_worker(task: EvalTask, result_queue: mp.Queue, gpu_id: int): 评测 Worker 进程每个进程独立设置环境和模型实例 设计原因每个 worker 是独立进程有自己的 Python 解释器和全局状态 不会与其它 worker 共享任何可变状态 # 设计原因每个 worker 绑定特定 GPU避免跨 GPU 数据传输 # 同时设置环境变量确保底层 CUDA 库也使用正确设备 import os os.environ[CUDA_VISIBLE_DEVICES] str(gpu_id) # 设计原因每个 worker 独立设置随机数种子 # 使用 task_id 的 hash 来派生种子确保不同子任务的种子不同 # 但同一子任务的重复执行结果可复现 worker_seed hash(task.task_id) % (2**31) task.seed np.random.seed(worker_seed) torch.manual_seed(worker_seed) torch.cuda.manual_seed_all(worker_seed) # 设计原因每个 worker 加载独立的模型实例 # 模型权重在显存中只读但推理状态KV Cache独立 # 如果显存不足可以用 torch.multiprocessing 的共享内存加载一次权重 model load_model(task.model_path).cuda() model.eval() # 设计原因每个 worker 读取自己的数据分片 # 使用 task_id 作为分片标识避免数据重叠 dataset load_dataset(task.dataset_path, shard_idtask.task_id) results [] for batch in DataLoader(dataset, batch_sizetask.batch_size): with torch.no_grad(): outputs model(batch) results.extend(compute_metrics(outputs)) # 设计原因通过 Queue 传递结果而非写共享文件 # 避免文件锁竞争和写入冲突 result_queue.put({ task_id: task.task_id, results: results, seed_used: worker_seed }) class ParallelEvaluator: 并行评测调度器 def __init__(self, num_gpus: int 4, tasks_per_gpu: int 1): # 设计原因总并发数 GPU数 × 每GPU任务数 # 每GPU运行超过1个任务时模型显存要能容纳多份实例 self.max_workers num_gpus * tasks_per_gpu self.num_gpus num_gpus # 设计原因使用 Manager.Queue 而非普通 Queue # Manager.Queue 支持跨进程传递复杂对象 self.result_queue mp.Manager().Queue() def run(self, tasks: List[EvalTask]) - Dict[str, List]: 执行所有评测任务 results {} # 设计原因使用进程池管理 worker 生命周期 # 确保异常退出时资源被正确回收 with mp.Pool(processesself.max_workers) as pool: jobs [] for i, task in enumerate(tasks): gpu_id i % self.num_gpus # 设计原因异步提交任务非阻塞 # 这样 worker 池可以持续消费新任务 job pool.apply_async( eval_worker, args(task, self.result_queue, gpu_id) ) jobs.append(job) # 等待所有任务完成 for job in jobs: job.get(timeout3600) # 单任务超时1小时 # 设计原因从 Queue 收集结果后统一聚合 while not self.result_queue.empty(): result self.result_queue.get() results[result[task_id]] result[results] return results四、隔离粒度与资源效率的平衡并行评测的隔离策略需要在隔离程度和资源利用率之间权衡。完全进程隔离最安全每个子任务一个独立进程 独立模型实例。问题在于显存开销——加载 4 个 7B 模型实例需要 4×14GB ≈ 56GB 显存可能超出单卡容量。模型权重共享 推理状态隔离使用 torch.multiprocessing 的共享内存加载一份模型权重每个 worker 进程通过 fork 继承权重指针。显存利用率高但需要小心处理 CUDA 上下文fork 后 CUDA 上下文不能继承需在 fork 后重新初始化。这是大多数场景的推荐方案。MIG 分区在 A100/H100 上使用 MIG 将 GPU 切分为独立的计算实例。每个实例有独立的显存和计算资源隔离性最强但硬件要求高。容器化隔离使用 Docker 容器为每个子任务提供完全隔离的环境。扩容灵活适用于大规模评测集群但容器启动和镜像拉取有额外时间成本。选择建议单卡评测 10 个子任务进程隔离每个 GPU 1~2 个 worker多卡评测10~100 个子任务模型权重共享 推理状态隔离大规模评测 100 个子任务容器化 Kubernetes 调度。五、总结评测任务并行化的核心挑战在于数据隔离和状态隔离。随机数种子、模型推理状态、文件系统写入是三个最容易被忽略的共享状态。多进程 Worker Pool 架构通过独立进程实现隔离但需要仔细处理 CUDA 上下文在 fork 后的重建。隔离粒度的选择存在安全性与资源效率的权衡——完全进程隔离最安全但显存开销大模型权重共享方案效率高但需要额外的状态隔离处理。对于不同规模的评测场景应选择匹配的隔离策略。