ORBIT:远程 GPU 运行
· 12 分钟

ORBIT 的内核为什么不懂任务

目录
  1. 瓶颈剖析:执行器的认知诅咒
  2. 架构重组:任务不可知的通用核
  3. 结果证明:用模板镇压分支地狱

ORBIT 起初只跑训练,后来加入评测和数据采集。每加一种任务,我就往 runner 里加一条分支。这样接入很快,但任务各自的配置和处理逻辑也都堆进了执行器。

我随后限制了执行核的职责:只处理 bundle、放置策略、启动模式和产物收集。训练、评测和采集的具体含义由 TaskPlugin 解释,以减少新增任务对执行器的影响。

瓶颈剖析:执行器的认知诅咒

如果 traineval 各有自己的暂存路径,接入第四种任务时就得修改共享的执行代码,原来的任务也可能因此出错。这违反了开闭原则 (Meyer, 1988):每次扩展新任务,都需要修改核心执行器。

这个代价可以用数学量化。如果在共享启动路径里加 if task == 分支,测试表面积就会呈组合级数膨胀:NN 种任务类型 ×\times 共享代码路径的条件复杂度。这违背了 David Parnas (1972) 在《On the Criteria To Be Used in Decomposing Systems into Modules》 中提出的基本原则:按“什么在变”而非按处理步骤来切分系统。

比较这些任务后,我把职责分成两组:

  • 各任务不同的部分:请求的形状、合法配置的定义、产物汇总逻辑。
  • 各任务共用的部分:暂存 bundle、启动、监控进程、收集日志和产物。

前一组负责整理请求和读取结果,交给插件;后一组保留在执行器中。

架构重组:任务不可知的通用核

插件、控制核和执行核的关系如下:

+-------------------+      +-------------------+      +-------------------+
| Training Plugin   |      | Evaluation Plugin |      | Collection Plugin |
| (Knows dataset)   |      | (Knows models)    |      | (Knows targets)   |
+--------+----------+      +--------+----------+      +--------+----------+
         |                          |                          |
         v                          v                          v
+-------------------------------------------------------------------------+
| Control Core (Task-Agnostic)                                            |
| [Template + Overrides] -> Execution Request -> Generic Bundle           |
+-------------------------------------------------------------------------+
         |
         v
+-------------------------------------------------------------------------+
| Execution Core                                                          |
| Provision -> Placement -> Launch -> Collect (Blind to inner payload)    |
+-------------------------------------------------------------------------+

插件接口定义在 orbit/core/control/registry.py 中的 TaskPlugin 协议,共有四个方法:

class TaskPlugin(Protocol):
    task_type: str
    job_kind: JobKind

    def parse_request(self, raw: dict | Any) -> Any: ...
    def validate_request(self, request: Any) -> list[str]: ...
    def build_bundle(self, *, bundle_dir: str, submission: TaskSubmission) -> JobBundle: ...
    def summarize_result(self, *, submission, bundle, status, manifest) -> TaskSummary: ...

执行核只认识 JobBundleTaskSummary。在 orbit/tasks/training/plugin.py 中,TrainingPlugin 校验 dataset_pathoutput_dir;而在评测插件里校验的则是 environments。这些任务字段不会进入执行核。

控制内核严格依赖显式的插件注册,从不直接 import 任务代码。接线被限制在唯一的组合根 build_default_task_registry 中,局部 import 避免了任何全局副作用。

结果证明:用模板镇压分支地狱

为了消灭隐藏的条件分支,我将运行时的一切变更限制在“模板加覆盖 (overrides)”的范畴内。控制核通过 ExecutionTemplateRegistry.resolve 将提交解析为带有白名单 diff 的 ExecutionRequest

如果设计出错,就会遭遇 Joel Spolsky 提出的“漏抽象”定律 (The Law of Leaky Abstractions, 2002):一个所谓的通用字段其实暗含了特定任务的假设。为避免这类问题,我坚持不在内核中写任何 if。新增任务的修改范围被限制在单个插件内部,各任务继续共用执行器的监控和产物回收逻辑。

llminfrastructurearchitectureorbitdesign

评论