Skip to content

feat(jobs): continue_from —— 续跑用 job 引用,客户端不再回传 token(结果 token+logprob 不变) - #21

Open
Chronostasys wants to merge 3 commits into
b300-glm52from
feat/jobs-continue-from
Open

Chronostasys wants to merge 3 commits into
b300-glm52from
feat/jobs-continue-from

Conversation

@Chronostasys

@Chronostasys Chronostasys commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

这个 PR 做什么

训练 job 侧的两个问题一起解决,3 个 commit:

commit 内容
1d2f79beff feat(jobs) 续写 API continue_from:续写只发 job 引用,客户端不再回传 token
1b0dfb79be fix(lora/pd) PD 下禁止请求内隐式重载 adapter —— 根治 600s bootstrap 静默死等(引擎 + jobs 两处代码修复)
174691dcfb docs(jobs) 续写 API 上手文档 docs/agent/jobs-continue-api.md

已在集群(B300 1021 prefill / 1022 decode + smg)全量部署并复测通过


1. continue_from:续写不再回传 token(1d2f79beff)

1.1 两个方向别搞混

  • 结果方向完全没变output_text / output_ids / output_token_logprobs 照常返回(那是训练数据本身,本 PR 一行未动)。
  • 只省掉请求方向:续写时客户端不再把「原输入 + 已生成」的几万~几十万 token 塞进请求体 —— 之前那条路既大又容易在 thinking 段/特殊 token 边界漂移一两个 token。

1.2 接口(输入三选一,必须且只能一个)

# job 级续写:请求体只有一个 id
curl -X POST $BASE/v1/control/jobs -H "Authorization: Bearer $KEY" \
  -d '{"continue_from": {"job_id": "job_x"}, "max_tokens": 512}'
// 任务级:可指定 task_id / index / sample_index
[{"continue_from": {"job_id": "job_x", "task_id": "job_x_t0000"}, "max_tokens": 512}]
  • 输入 = 源任务输入 token ++ 源任务已生成 token(token 级拼接,不经过文本);
  • temperature / top_p / lora_path 省略时继承源任务(同 adapter、同分布);max_tokens 不继承;
  • 运行中的源也能续(取实时 partial);源无生成内容 → 明确报错,不产空样本;
  • 续写结果只含本次新增 token。

1.3 关键实现取舍

需要 采用 否掉的替代方案
源 prompt 的 token id 引擎 /v1/tokenize(引擎自己的 tokenizer) ① 落盘 prompt ids = 每个任务重复整段 prompt;② 引擎 return_prompt_token_ids 每个 streaming chunk 都带tokenizer_manager.py:2075/2114)→ 长 prompt 放大成几百 MB
「续写的续写」保真 组合后的输入落盘 input_XXXX.json ——
历史数据 新增字段全 #[serde(default)];文本 prompt 由引擎 tokenizer 现场还原 需要迁移/重放历史 job

单测 +11(body 构造 / token 拼接 / sample 选择 / 输入三选一 / continue_from 解析 / job 级展开与继承 / 历史 job.json 恢复且可续跑 / 未知与歧义源报错),cargo test --lib control_plane::jobs:: 17/17,全量 lib 434/434


2. PD 下 adapter 缺失 → 600s 静默死等(1b0dfb79be)

2.1 症状

任何引用「当前引擎未加载」adapter 的请求在 PD 上静默挂 10 分钟再失败:

prefill:Prefill bootstrap failed ... KVTransferError(bootstrap_room=...):
         Request ... timed out after 600.0s in KVPoll.Bootstrapping
jobs   :task failed, error=empty SSE response(0 token)
decode :该 bootstrap_room/rid 零记录;num_queue_reqs=0(请求没进调度器)

2.2 根因(逐层证据)

  1. KV 索引协商与 radix 无关:KV 池在 decode,decode 必须先分配槽位并把 dst_kv_indicessend_metadata 回给 prefill(disaggregation/decode.py::pop_preallocated);--disable-radix-cache 只让命中=0(全量传),"分配+回索引"任何 PD 请求都躲不掉 ⇒ KVPoll.Bootstrapping 超时 = 该回索引的一侧没回。
  2. adapter 未加载时,prefill 与 decode 都在请求路径内隐式重载(日志 Reloading evicted adapterStart load Lora adapter);decode 在加载期间不分配、不回索引 → prefill 干等 → 600s 超时。
  3. 与续写无关:普通 job(不带 continue_from)+ 同一 lora_path 一样卡;不带 adapter 的 job 全部正常。continue_from 只是「继承 lora_path」更容易撞上。

2.3 修复(两处,均为代码层)

  • 引擎(根因) managers/tokenizer_manager.py::_resolve_lora_pathdisaggregation_mode 非空时禁止隐式重载,直接 400 并给出 POST /load_lora_adapter 指引 —— 请求内加载无法保证两个引擎都就绪,只会把握手拖到超时。
  • jobs(防线) sgl-model-gateway/src/control_plane/jobs.rs:带 lora_path 的任务派发前 ensure_lora_loaded(逐引擎 GET /v1/models)预检,缺失即 fail 该任务并附同一条指引(fail_task_preflight 顺带统一了输入解析失败的失败路径)。

3. 验证(集群实测,非推断)

# 用例 结果
1 prefill / decode / smg 健康 ✅ 200 / 200 / 200
2 结果方向不变 ✅ baseline job output_ids=24 且与 output_token_logprobs 逐位对齐
3 continue_from(无 adapter) prompt_tokens=32 == 8 + 24
4 adapter 未加载时提交 3 秒失败(旧行为 600s 静默),error 含 load_lora_adapter 指引
5 显式加载 adapter 到双端 → adapter 任务 ✅ completed(各 14s 加载)
6 adapter 续写(continue_from) prompt_tokens=89 == 25 + 64
7 历史 job(上线前落盘)续写 prompt_tokens=987 == 21 + 966
8 重启后 job 恢复 ✅ 67 个 job 从磁盘恢复

4. 文档

  • docs/agent/jobs-continue-api.md新增,面向训练侧的续写 API 上手手册:两个方向、字段表、继承规则、自检锚点、cancel→续写 Python 示例、错误码、PD 下 adapter 前置条件、成本、与裸 /v1/completions 对比)
  • docs/agent/training-jobs-api.md(§10 续写章节、§8 运维变量)
  • docs/agent/lora-pd-implicit-reload-stall.md新增,PD adapter 卡死根因/证据/判据/运维铁律)
  • AGENTS.md(§8 新增该 bug 的 gotcha;§9 索引补两条)

5. 部署状态与注意事项

  • 引擎侧改动已部署到 1021 prefill + 1022 decode(rsync tokenizer_manager.py、清 __pycache__、PD 配对重启);smg 已换新二进制并重启(job 注册表从磁盘恢复)。
  • PD 下 adapter 必须先显式加载到每个引擎,不要依赖「请求触发自动加载」(现在会秒级 400 并给出指引)。
  • 其它集群(1102/1104、B200 2P2D)若也要该保护,需同样 rsync + 配对重启(未包含在本 PR 的部署范围内)。

@github-actions github-actions Bot added documentation Improvements or additions to documentation model-gateway lora labels Sep 14, 2026
训练 job 现场:取消(或 max_tokens 用尽)后想接着生成,客户端过去的做法是把
"原输入 + 已生成" 的 token 数组回传。那条路有两个问题:

1. 体积:几万~几十万 token 塞进请求体(prompt 部分往往是主体);
2. 保真:必须客户端自己保证逐位正确;一旦按文本回传,重新 tokenize 会在
   thinking 段 / 特殊 token 边界漂移一两个 token,续跑就不是同一条轨迹。

本次把拼接移到服务端:客户端只发引用。

新增输入三选一(必须且只能一个,否则 400):
  * prompt        文本(原行为)
  * input_ids     token 数组(原计划,仍支持;引擎不再 tokenize,逐位确定)
  * continue_from 引用已有任务(本次主入口)

continue_from 支持两级:
  * 任务级 [{"continue_from": {"job_id": ..., "task_id"|"index": ..., "sample_index": ...}}]
  * job 级 {"continue_from": {"job_id": ...}, "max_tokens": ...} —— 与源 job 任务
    一一对应展开,是最小请求体(客户端一个 id 就够)

语义与实现要点:
  * 输入 = 源任务输入 token ++ 源任务已生成 token(token 级拼接,不经过文本);
  * 源 prompt 的 token id 由**引擎自己的 tokenizer** 现场还原(新增
    JobManager::tokenize_prompt → 引擎 /v1/tokenize),既不落盘重复整段
    prompt,也不依赖引擎可选的 prompt_token_ids 回显(该字段每个 chunk 都带,
    长 prompt 会放大成几百 MB);
  * 续跑任务的组合输入落盘 input_XXXX.json,因此"续跑的续跑"同样逐位精确,
    不受源任务后续变化影响;
  * 采样/adapter 继承:temperature / top_p / lora_path 省略时继承源任务
    (同 adapter、同分布),max_tokens 不继承(新预算);
  * 运行中的源也能续(取实时 partial 快照);源无任何生成时给出明确错误;
  * 历史数据兼容:上线前已落盘的 job.json / task_XXXX.json 无需迁移即可作为源
    (文本 prompt 走 tokenize 还原,结果文件照旧参与拼接);
  * 结果方向不变:output_ids + 逐 token logprob 照常回传(训练数据本身),
    续跑任务的结果只含本次新增 token。

其它:
  * run_one_sample 改收预构造 body(token 输入与文本输入共用一条路径);
  * server.rs 把 prefill/decode worker URL 传给 JobManager,可用
    SMG_JOBS_ENGINE_URLS 覆盖;
  * 单测 +11(body 构造 / 拼接 / sample 选择 / 输入三选一 / continue_from 解析 /
    job 级展开与继承 / 历史 job.json 恢复与可续跑 / 未知与歧义源报错)。
@Chronostasys
Chronostasys force-pushed the feat/jobs-continue-from branch from 3e0ead0 to 1d2f79b Compare September 14, 2026 07:30
症状(2026-09-14 实测):任何引用「当前引擎未加载」adapter 的 /generate 请求在 PD 上
静默挂 10 分钟后失败:

  prefill:Prefill bootstrap failed ... KVTransferError(bootstrap_room=...):
           Request ... timed out after 600.0s in KVPoll.Bootstrapping
  jobs   :job failed, error=empty SSE response(0 token)
  decode :该 bootstrap_room / rid 零记录;num_queue_reqs=0(请求没进调度器)

根因(逐层证据):
  1. KV 索引协商与 radix 无关:KV 池在 decode,decode 必须先分配槽位并把
     dst_kv_indices 经 send_metadata 回给 prefill(decode.py::pop_preallocated),
     prefill 才知道往哪写。--disable-radix-cache 只让命中长度=0(全量传),
     「分配+回索引」任何 PD 请求都躲不掉。
  2. adapter 未加载时,prefill 与 decode 都在【请求路径内】隐式重载
     (日志 Reloading evicted adapter -> Start load Lora adapter);
     decode 在加载期间不分配、不回索引 -> prefill 干等 -> 600s 超时。
  3. 与 continue_from 无关:普通 job(不带续跑)+ 同一 lora_path 一样卡;
     无 adapter 的 job 全部正常(continue_from 只是继承 lora_path 更易撞上)。

修复(两处,均为代码层):
  A. 引擎(根因)managers/tokenizer_manager.py::_resolve_lora_path:
     disaggregation_mode 非空(prefill/decode)时禁止隐式重载,直接 400 并给出
     可执行指引(POST /load_lora_adapter {lora_name,lora_path})。显式加载才能
     保证两个引擎都拿到 adapter,请求内加载无法保证。
  B. jobs 层(防线)sgl-model-gateway/src/control_plane/jobs.rs:
     任务带 lora_path 时,execute_task 先逐引擎 GET /v1/models 预检
     (ensure_lora_loaded / engine_has_lora),缺失即立即 fail 该任务并附同一条
     指引(fail_task_preflight 顺带统一了输入解析失败的失败路径)。
     10 分钟静默等待 -> 立刻可执行的错误。

验证:cargo test --lib control_plane::jobs:: 17/17;py_compile 通过。
文档:docs/agent/lora-pd-implicit-reload-stall.md(机制/证据/判据/运维铁律)
      + AGENTS.md §8 gotcha。
@Chronostasys

Copy link
Copy Markdown
Collaborator Author

fix(lora/pd): PD 下禁止请求内隐式重载 adapter —— 根治 600s bootstrap 静默死等

症状(2026-09-14 实测):任何引用「当前引擎未加载」adapter 的 /generate 请求在 PD 上
静默挂 10 分钟后失败:

prefill:Prefill bootstrap failed ... KVTransferError(bootstrap_room=...):
Request ... timed out after 600.0s in KVPoll.Bootstrapping
jobs :job failed, error=empty SSE response(0 token)
decode :该 bootstrap_room / rid 零记录;num_queue_reqs=0(请求没进调度器)

根因(逐层证据):

  1. KV 索引协商与 radix 无关:KV 池在 decode,decode 必须先分配槽位并把
    dst_kv_indices 经 send_metadata 回给 prefill(decode.py::pop_preallocated),
    prefill 才知道往哪写。--disable-radix-cache 只让命中长度=0(全量传),
    「分配+回索引」任何 PD 请求都躲不掉。
  2. adapter 未加载时,prefill 与 decode 都在【请求路径内】隐式重载
    (日志 Reloading evicted adapter -> Start load Lora adapter);
    decode 在加载期间不分配、不回索引 -> prefill 干等 -> 600s 超时。
  3. 与 continue_from 无关:普通 job(不带续跑)+ 同一 lora_path 一样卡;
    无 adapter 的 job 全部正常(continue_from 只是继承 lora_path 更易撞上)。

修复(两处,均为代码层):
A. 引擎(根因)managers/tokenizer_manager.py::_resolve_lora_path:
disaggregation_mode 非空(prefill/decode)时禁止隐式重载,直接 400 并给出
可执行指引(POST /load_lora_adapter {lora_name,lora_path})。显式加载才能
保证两个引擎都拿到 adapter,请求内加载无法保证。
B. jobs 层(防线)sgl-model-gateway/src/control_plane/jobs.rs:
任务带 lora_path 时,execute_task 先逐引擎 GET /v1/models 预检
(ensure_lora_loaded / engine_has_lora),缺失即立即 fail 该任务并附同一条
指引(fail_task_preflight 顺带统一了输入解析失败的失败路径)。
10 分钟静默等待 -> 立刻可执行的错误。

验证:cargo test --lib control_plane::jobs:: 17/17;py_compile 通过。
文档:docs/agent/lora-pd-implicit-reload-stall.md(机制/证据/判据/运维铁律)
+ AGENTS.md §8 gotcha。

面向训练侧同学的快速上手文档 docs/agent/jobs-continue-api.md:

* 开篇钉死两个方向:结果方向(output_text/output_ids/output_token_logprobs)
  完全没变 —— 那是训练数据;省掉的只是【续写请求方向】的 token 回传。
* 30 秒上手(普通 job -> 只发 job_id 续写),job 级 / 任务级字段表,
  sample_index 用法,参数继承规则(temperature/top_p/lora_path 继承源任务,
  max_tokens 不继承)。
* 自检锚点 prompt_tokens == 源 prompt_tokens + len(源 output_ids),
  附实测数据 32=8+24 / 987=21+966 / 89=25+64。
* cancel -> 续写 端到端 Python 示例;错误码表;PD 下 adapter 必须先显式加载到
  每个引擎(未加载现为秒级失败 + 指引,旧行为 600s 静默);成本分析
  (已生成 token 必须重 prefill + 整个上下文 KV 传输);与裸 /v1/completions 对比。
* AGENTS §9 索引补该文档条目。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation lora model-gateway

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants