feat(jobs): continue_from —— 续跑用 job 引用,客户端不再回传 token(结果 token+logprob 不变) - #21
Chronostasys wants to merge 3 commits into
Conversation
训练 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 恢复与可续跑 / 未知与歧义源报错)。
3e0ead0 to
1d2f79b
Compare
症状(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。
|
fix(lora/pd): PD 下禁止请求内隐式重载 adapter —— 根治 600s bootstrap 静默死等 症状(2026-09-14 实测):任何引用「当前引擎未加载」adapter 的 /generate 请求在 PD 上 prefill:Prefill bootstrap failed ... KVTransferError(bootstrap_room=...): 根因(逐层证据):
修复(两处,均为代码层): 验证:cargo test --lib control_plane::jobs:: 17/17;py_compile 通过。 |
面向训练侧同学的快速上手文档 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 索引补该文档条目。
这个 PR 做什么
训练 job 侧的两个问题一起解决,3 个 commit:
1d2f79beffcontinue_from:续写只发 job 引用,客户端不再回传 token1b0dfb79be174691dcfbdocs/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 一行未动)。1.2 接口(输入三选一,必须且只能一个)
temperature/top_p/lora_path省略时继承源任务(同 adapter、同分布);max_tokens不继承;1.3 关键实现取舍
/v1/tokenize(引擎自己的 tokenizer)return_prompt_token_ids每个 streaming chunk 都带(tokenizer_manager.py:2075/2114)→ 长 prompt 放大成几百 MBinput_XXXX.json#[serde(default)];文本 prompt 由引擎 tokenizer 现场还原单测 +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 分钟再失败:
2.2 根因(逐层证据)
dst_kv_indices经send_metadata回给 prefill(disaggregation/decode.py::pop_preallocated);--disable-radix-cache只让命中=0(全量传),"分配+回索引"任何 PD 请求都躲不掉 ⇒KVPoll.Bootstrapping超时 = 该回索引的一侧没回。Reloading evicted adapter→Start load Lora adapter);decode 在加载期间不分配、不回索引 → prefill 干等 → 600s 超时。continue_from)+ 同一lora_path一样卡;不带 adapter 的 job 全部正常。continue_from只是「继承 lora_path」更容易撞上。2.3 修复(两处,均为代码层)
managers/tokenizer_manager.py::_resolve_lora_path:disaggregation_mode非空时禁止隐式重载,直接 400 并给出POST /load_lora_adapter指引 —— 请求内加载无法保证两个引擎都就绪,只会把握手拖到超时。sgl-model-gateway/src/control_plane/jobs.rs:带lora_path的任务派发前ensure_lora_loaded(逐引擎GET /v1/models)预检,缺失即 fail 该任务并附同一条指引(fail_task_preflight顺带统一了输入解析失败的失败路径)。3. 验证(集群实测,非推断)
output_ids=24且与output_token_logprobs逐位对齐continue_from(无 adapter)prompt_tokens=32 == 8 + 24load_lora_adapter指引prompt_tokens=89 == 25 + 64prompt_tokens=987 == 21 + 9664. 文档
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. 部署状态与注意事项
tokenizer_manager.py、清__pycache__、PD 配对重启);smg 已换新二进制并重启(job 注册表从磁盘恢复)。