ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OpenResearch 实验的 Kubernetes 后端实战:从 Manifest 契约到 kubectl 源码实现

2026/9/19 18:51:37 拓冰建站 浏览量
OpenResearch 实验的 Kubernetes 后端实战:从 Manifest 契约到 kubectl 源码实现 OpenResearch 实验的 Kubernetes 后端实战从 Manifest 契约到 kubectl 源码实现【免费下载链接】OpenResearchTurn your coding agents into research agents项目地址: https://gitcode.com/GitHub_Trending/op/OpenResearch导读本文以 OpenResearch 项目GitHub_Trending/op/OpenResearch将你的编码 Agent 变成研究 Agent 的实验编排系统中 Kubernetes 后端参考文档 为骨架结合 k8s 后端源码 与 本地提交实现系统讲解orx exp run --backend k8s的完整用法何时使用该后端、如何配置 kubeconfig 与 Kubernetes profile、如何编写一份可被 orx 接收的 Job Manifest、提交时契约lint 规则与自动注入的底层原理以及任务状态跟踪、日志流与取消清理的完整生命周期。读完本文你将能独立编写并提交符合 orx 契约的 Kubernetes 实验运行 Manifest并理解其“Manifest 即代码、快照不可变”的设计哲学。何时使用 Kubernetes 后端Kubernetes 后端--backend k8s是 orx 的一个远程计算后端。根据 k8s.md 的约定只应在以下两种情况使用用户明确请求使用 Kubernetes / K8sKubernetes 是当前会话的配置默认后端。认证完全来自用户本机的 kubeconfig 文件context 与 namespace 则来自用户配置的 Kubernetes profile详见下文“认证与配置”。连接了某个集群的凭据本身不是切换到 k8s 后端的信号——需要像 orx-compute SKILL 强调的那样遵循会话剧本session playbook声明的默认后端仅当用户点名时才切换到其他后端。一个重要的特性是k8s 后端没有 flavorsflavor 是 hf/modal 等后端的硬件档位。运行形态是一个提交在实验分支上的 Kubernetes Manifest因此计算形态镜像、GPU、拓扑、资源请求与实验代码一样是可版本化、可 diff 的仓库内文件。正如 src/jobs/kubernetes.rs 的模块注释所说“orx owns only the run contract, not the shape”orx 只拥有运行契约不拥有形态。认证与配置kubeconfig k8s.json认证直接走 kubectl整个 k8s 后端的所有集群操作都通过kubectl二进制完成而不是使用 Kubernetes 客户端 crate。这带来两个关键收益见 src/jobs/kubernetes.rs零依赖成本不引入任何 Kubernetes 客户端依赖认证原样继承用户的 kubeconfig 认证方式包括 exec 插件、token、client-certificate 等被原样复用。尤其重要的是依赖 exec 插件的托管集群如 CoreWeave、EKS、GKE无需额外适配。kubectl 调用封装 展示了这一点kubectl位于 PATH 上是硬前提若未找到报错信息会明确提示“install it to use --backend k8s”调用时会按需附加--context并将 stdin 透传给 kubectl用于 Secret 与 Manifest 的apply -f -/create -f -保证敏感内容不落在命令行上。配置K8sSettings后端配置存储在$XDG_CONFIG_HOME/openresearch/k8s.json无密钥——所有认证都在 kubeconfig 里。结构定义在 src/jobs/kubernetes.rs字段类型默认值含义contextOptionStringNonekubeconfig 中的 context 名称None表示使用 kubectl 当前的 current-contextnamespaceStringdefault运行 Job 的目标 namespace配置可通过orx up的 Settings → Compute 界面修改或直接编辑该 JSON 文件。若文件缺失或不可读load_settings返回Ok(None)视为 k8s 未配置行为等同默认值若文件内容损坏则报错提示“Fix or delete it and reconfigure”。启动前预检Preflight在提交运行之前preflight 会做两件事kubectl version --client—— 确认 kubectl 可用kubectl auth can-i create jobs -n namespace—— 通过一次往返同时探测 API Server 可达性与权限。can-i在 stdout 上输出yes/no都表示 API Server 已响应no时进程退出码为 1所以此处绕过退出码检查直接读输出。backend_adapter 的 KubernetesCompute 定义 把预检结果汇总为kubectl_found/reachable/can_create_jobs三项任一不满足都判定为未就绪not ready并给出明确原因。这些信息也会出现在orx up的启动警告与设置界面中。运行形态Manifest 即代码k8s 后端没有 flavors也没有--imagesrc/local/k8s.rs 会直接拒绝--flavor与--image参数。运行形态完全由一个提交在实验分支上的 Manifest声明默认路径.orx/k8s.yaml常量定义见 src/local/k8s.rs覆盖方式--manifest path仓库相对路径。Manifest 在记录recordedcommit 的不可变快照上读取而不是从当前工作区读取。实现位于 src/local/k8s.rs通过git::file_at(repo_path, revision, path)取指定修订版的文件内容若该修订版中不存在 Manifest会直接报错并提示“write one and commit it before running”——未提交的 Manifest 编辑永远不会被执行。这与 orx 的核心设计一致每个后端都运行实验记录 commit 的不可变源码快照未提交文件一律排除。命令行操作# 使用配置默认后端直接运行 orx exp run expId --backend k8s # 指定 Manifest 路径与超时 orx exp run expId --backend k8s --manifest infra/run.yaml --timeout 8horx exp run将运行排队后立即返回队列化提交随后可用orx runs、orx logs runId、orx exp wait expId或orx exp wake expId跟进。--force允许在同一实验上故意并发运行不带它时若该节点上已有运行在途orx 会拒绝再次启动见 orx-compute SKILL.md 的 Universal launch contract。提交成功后launch_local_k8s 会打印摘要job namespace/jobname、使用的 manifest 路径、已创建的资源列表kind/name以及 run id并提示“日志跟随主 Job 的 leader pod”。参数细节来自 CLI 定义--timeoutk8s 后端中该值成为activeDeadlineSeconds除非 Manifest 自己设置了该字段见 src/main.rs。时间格式支持90s、30m、4h、1d解析实现见 huggingface::parse_timeout未传时的默认值是4 小时src/local/k8s.rs 的注释点明了理由无超时的 Job 是安全隐患与 HF 路径保持一致。--manifest仅适用于--backend k8s其他后端会拒绝该参数见 src/compute.rs 及各本地提交模块的校验。最小 Manifest 示例k8s.md 给出的最小可运行 Manifest 如下apiVersion: batch/v1 kind: Job metadata: name: train-{{ORX_RUN}} spec: template: spec: restartPolicy: Never containers: - name: run image: pytorch/pytorch:2.6.0-cuda12.4-cudnn9-runtime command: [bash, -c, $ORX_SCRIPT] resources: requests: { nvidia.com/gpu: 4, cpu: 32, memory: 128Gi } limits: { nvidia.com/gpu: 4 }逐字段解读metadata.name: train-{{ORX_RUN}}{{ORX_RUN}}是提交时被替换的占位符。orx 从 run id 提取至多 10 位 ASCII 十六进制字符生成一个DNS 安全、运行唯一的 token见 src/local/k8s.rs替换到 Manifest 文本中从而保证重复运行不会产生资源名冲突。建议在所有资源名中包含它。restartPolicy: NeverJob 的标准要求配合 orx 注入的backoffLimit: 0容器失败即 Job 失败不会因重启导致与已暂存的 pod 本地快照不一致。command: [bash, -c, $ORX_SCRIPT]必须引用 orx 注入的ORX_SCRIPT环境变量。该变量内容由 gated_script 生成先等待快照就绪文件/tmp/orx-source/source.tar.ready出现再解压/tmp/orx-source/source.tar到repo/目录并cd进入最后执行实验的固定 run command。这是“快照不可变、命令固定”契约在容器内的落点。resources.requests/limitsGPU 数量、CPU、内存完全由你声明。k8s 后端不会为你做资源抽象因此 k8s.md 提醒在挑选资源之前先检查集群现状如kubectl describe nodes、kubectl top nodes、检查 GPU 调度器与 device plugin 是否就绪再决定 requests 与 limits。示例按惯例只对 GPU 设 limitsGPU 不可超卖CPU/内存的 limits 可按需添加。提交时契约submit-time contractk8s.md 定义的提交时契约在源码层面由 prepare_docs 以“响亮失败”loud的 lint 规则 自动注入实现。所有检查都在客户端预检kubectl create --dry-runclient -o json见 run_manifest之后进行既完成 YAML→JSON 转换与 schema 合理性检查又避免在集群上做破坏性实验。契约一恰好一个主 JobManifest 中至少包含一个 Job——Job 的完成/失败就是这次 run 的结果。没有 Job 直接报错测试no_job_is_an_error验证了这一点。若有多个 Job必须用标签orx-primary: true常量PRIMARY_LABEL见 src/jobs/kubernetes.rs恰好标记其中一个否则报错并提示标记方式测试two_jobs_need_a_primary_label验证。Parallel 与 Indexed Job 被拒绝spec.completions 1、spec.parallelism 1或completionMode: Indexed都会报错src/jobs/kubernetes.rs。原因正如 k8s.md 所述不可变归档源码快照被暂存进单个 pod多 pod 并行无法安全共享这份 pod 本地快照。契约二容器必须执行$ORX_SCRIPT主 Job 中恰好一个容器在command或args中引用ORX_SCRIPT没有引用或引用数量多于一个都会报错测试job_must_reference_the_script验证。orx 会用真正的脚本内容覆盖容器 env 中的ORX_SCRIPT用户写入的“evil”值被替换见测试author_orx_script_env_is_replaced_and_secret_not_duplicated为该容器设置注解kubectl.kubernetes.io/default-container让kubectl logs默认跟随运行实验的那个容器当容器为 Python 时注入两个默认 envPYTHONUNBUFFERED1让kubectl logs -f能实时看到输出避免输出被缓冲与PYTHONIOENCODINGutf-8若 Manifest 显式设置了这些变量作者的值优先且不会重复注入测试python_env_defaulted_and_author_value_wins验证。契约三每个资源必须有名字每个资源都需要metadata.namegenerateName不被支持——orx 必须确切知道它创建了什么才能在取消时精确清理。违反时报错并提示用{{ORX_RUN}}防止重跑冲突测试generate_name_and_foreign_namespace_are_errors验证。不得在 Manifest 中设置与配置不符的 namespace任何资源若声明了与运行目标 namespace 不同的metadata.namespace会被拒绝--namespace由配置决定Manifest 不写 namespace 即可。契约四orx 的自动注入当 Manifest 未显式设置时orx 会为主 Job 注入以下默认值测试single_job_gets_defaults_labels_script_and_secret与author_deadline_wins_but_retries_are_disabled验证字段注入值说明spec.activeDeadlineSeconds--timeout解析值默认 144004hManifest 显式设置时作者值优先spec.ttlSecondsAfterFinished86400结束后 1 天自动清理避免遗留spec.backoffLimit0始终强制即使作者写了 2 也会被覆盖——重启 pod 无法共享已暂存的 pod 本地快照此外还会在所有资源的metadata.labels与主 Job pod 模板标签上注入运行标签or_runrunId、or_experimentexpId、or_projectprojectId标签内容来自 src/local/k8s.rs便于用kubectl get pods -l or_run...检索。契约五orx-envSecret 与环境注入orx 将本次运行所需的环境变量用户同步的 API keys 等见crate::config::list_synced_env以及自动解析的HF_TOKEN同步进一个namespace 内的 Secretorx-env常量ENV_SECRETsrc/jobs/kubernetes.rs。Secret 通过 stdin 以stringData形式kubectl apply -f -值绝不落在命令行上run_manifest。每次启动都会重新同步。主 Job 的所有容器自动获得envFrom引用orx-envoptional: trueenv 文件为空也没问题辅助资源见下节需要密钥时必须自己引用该 Secret。提交与资源暂存源码级调用链一次orx exp run expId --backend k8s的完整链路如下本地编排submit_local_k8s_with_source 读取实验/project 的固定 run command实验分支上设置的若为空则回退到 project 级均没有则报错提示先设置从记录 commit 中读取 Manifest构造注入脚本Secret 同步run_manifest先同步orx-envSecret占位符替换与预检替换{{ORX_RUN}}然后kubectl create --dry-runclient -n ns -f - -o json一次完成 YAML→JSON 与 schema 校验且不触碰集群多文档 Manifest 会以连续 JSON 对象流返回不是 List代码按流逐个解析run_manifest逐个创建 回滚lint 通过后逐个kubectl create -f -创建资源任一失败则按创建顺序逆序删除已创建项依赖先删保证不留半成品run_manifest源码快照暂存stage_source轮询等待 Job 的 pod 出现kubectl get pods -l job-namejob然后用kubectl cp把不可变源码归档传到 pod 的/tmp/orx-source/source.tar最后kubectl exec ... touch /tmp/orx-source/source.tar.ready发出就绪信号整个暂存阶段有120 秒期限超时报错并回滚全部资源。这正是容器内gated_script等待.ready文件的对应端——两者一内一外共同保证“pod 起来后、源码就绪前不会误跑”记录与监督提交句柄job、namespace、manifest 路径、资源清单被记录到 run随后spawn_detached_supervise分离出一个监督进程src/local/k8s.rs。运行生命周期状态映射、日志流与取消Job 状态 → 运行状态orx 把 Kubernetes Job 的阶段映射到与 HF 后端共享的阶段词汇表SCHEDULING/RUNNING/COMPLETED/ERROR/CANCELED/DELETED见 src/jobs/kubernetes.rs 模块注释使 jobs/mod.rs 中的stage_to_run_status与is_terminal_stage无需改动即可复用。inspect_job 的实现要点status.conditions中CompleteTrue或succeeded completions→COMPLETEDFailedTrue→ERROR并带上reason: message这正是orx exp wait失败输出中reason:行的来源存活 pod 数达到预期 →RUNNING否则 →SCHEDULING并透出 pod 的 waiting reason如ImagePullBackOff让“卡住”可被诊断Job 对象被删除NotFound→DELETED这既是取消的呈现方式也是 TTL 清理后的样子。日志流只跟 leader podk8s.md 约定“run log 跟随主 Job 的 sole pod”。实现上 leader_pod 优先选择带batch.kubernetes.io/job-completion-index: 0注解的 podIndexed Job 的场景否则按名称排序取第一个 pod而 stream_logs 通过kubectl logs -f pod/leader -n ns --tail-1拉流。只捕获单个 leader pod 是有意为之稳定的单流能保证行数去重replay/dedup机制在断线重连后依然正确——若交错多个 pod重连时日志顺序会被打乱。其他 pod 的输出仍可通过kubectl logs手动查看。日志流在空闲一段时间后断开由监督进程重新检查 Job 状态并决定是否重连。取消与清理orx exp cancel expId对应 delete_resources对 run 记录的资源句柄kind/name逆序执行kubectl delete handle -n ns --waitfalse依赖资源先于 Job 删除NotFound视为已消失。辅助资源伴随 Job 创建、取消时一并删除。监督进程不要杀它提交后分离的orx supervise进程通过 kubectl 持续观察 Job 直到终态并负责日志收集与状态落库。k8s.md 明确警告不要杀掉它——杀掉监督进程会导致运行结果与日志无法被 orx 正确回收。与其他后端的共性契约来自 orx-compute SKILL作为 orx 计算体系的一员k8s 后端同样遵循 orx-compute SKILL 的通用契约一律用orx exp run启动实验计算不要直接调用 kubectl/sbatch/ssh 或训练命令本身工作区只用于编辑、Git、编排与轻量检查直接启动的作业不受追踪可能运行的不是记录 commit 的代码保持 run command 固定在基线baseline上设置一次通过子分支变更代码或配置若无 run command先orx project edit projectId --run-command cmd提交后再启动每个后端都运行记录 commit 的不可变源码快照未提交文件被排除没有任何后端需要 GitHub pushorx exp run排队后立即返回随后用orx runs、orx logs、orx exp wait、orx exp wake跟进wait 是“睡到状态变化”的信号而非真相来源每次返回后都要读orx runs projectId对账所有新终态运行。契约的自动化验证测试即文档k8s 后端单元测试 将上述契约固化为可回归的断言是理解行为边界的第二份文档single_job_gets_defaults_labels_script_and_secret单 Job 被注入 namespace、标签、activeDeadlineSeconds14400、ttlSecondsAfterFinished86400、backoffLimit0、ORX_SCRIPT与orx-env引用author_deadline_wins_but_retries_are_disabled作者自定义 deadline 优先但 retries 一律被禁用list_with_aux_service_keeps_order_and_finds_the_jobList 形态的多资源文档顺序保留、标签注入到辅助资源、正确识别 Jobno_job_is_an_error/two_jobs_need_a_primary_label缺 Job、多 Job 未标记主 Job 都报错job_must_reference_the_script容器不引用ORX_SCRIPT即失败generate_name_and_foreign_namespace_are_errorsgenerateName与外部 namespace 都被拒绝。小结Kubernetes 后端是 orx 计算抽象中“最不抽象”的一员没有 flavor、没有拓扑旋钮有的只是一个提交在实验分支上的 Manifest 与一纸清晰、强制的契约。orx 负责认证继承kubeconfig、预检、快照暂存kubectl cp ready 文件握手、ORX_SCRIPT/orx-env/标签/默认值注入、状态映射、日志跟随与精确取消而你负责检查集群、在 Manifest 里声明镜像与资源、遵守“单 Job、有名字、无外部 namespace、引用$ORX_SCRIPT”的提交时规则。把计算形态当作代码版本化是这套设计最值得借鉴的地方。【免费下载链接】OpenResearchTurn your coding agents into research agents项目地址: https://gitcode.com/GitHub_Trending/op/OpenResearch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考