执行摘要
- 一句话:新增 Tinker 网关运行时与 serve 入口
- 推荐动作:值得精读,是本轮 multi-LoRA v2 栈中最靠近「真实执行链路」的一篇。阅读顺序建议:先看
miles/tinker/runtime.py 的 _pad_to_dp_multiple / _build_train_data / _execute_batch 三件套,理解 datum 契约与 sample_index 重组规则;再看 serve_tinker.py 的 serve(),重点体会任务监督(asyncio.wait(FIRST_COMPLETED))、dispose 顺序与 GatewayConfig 的推导;最后扫 miles/utils/arguments.py 的 entry 维度,确认对既有 train 入口没有隐性行为回归。需要重点关注的设计决策有四条:训练命令经 object store 传递并让 worker 自行按 DP 切分、用零掩码副本做 DP 补齐、显式失败透传与未知失败终止网关的失败分类边界、以及 NaN / MASK_LOGPROB 的 wire 约定。建议精读时反向确认 TrainerCell.execute(kill_on_failure=True) 与 Tinker 服务错误分类是否已完全对齐,并确认 _build_train_data 依赖首条 datum 探测可选键的假设在真实 SDK 用法下是否成立。
功能与动机
PR body 的表述是「Connect the protocol service to Miles through MilesBackend and serve_tinker.py」:dataset 驱动的 multi-LoRA v1 后端已被移除,改由 Tinker 协议承载——客户端对 adapter slot 显式提交 forward_backward / optim_step,服务端不再拥有数据集与调度(docs/advanced/lora.md 的改动原文:"Multi-LoRA training is served through the Tinker protocol: serve_tinker.py turns a miles deployment into a training service where each client drives its own adapter slot with explicit forward_backward / optim_step operations (the server owns no datasets or schedules)")。协议层与槽位机制在前序切片中已完成,本 PR 要解决的是「最后一公里」:训练命令如何落到 Megatron actor、采样如何经 router 命中不可变 adapter、失败如何分类并终止服务。由于 serve 场景没有 rollout/GBS 语义,还必须为参数解析新增 entry 维度,否则 --rollout-batch-size 等训练必填项会阻塞网关启动。
实现拆解
-
入口与参数契约(serve_tinker.py + miles/utils/arguments.py):parse_args 新增 entry 形参,断言只接受 "train" / "serve" 并写入 args.entry;add_miles_arguments 用 parser.set_defaults(entry="train") 兜底,保证既有调用方行为不变。--rollout-batch-size 从 required=True 降级为 default=None,其「必填」语义下沉到 miles_validate_args 中新增的 args.entry == "train" 分支,使 serve 入口可以不带任何 rollout 语义启动。新增 --tinker-server-port(默认 10613)、--tinker-base-model(默认 --hf-checkpoint)、--tinker-checkpoint-root(默认 <save>/tinker)三个网关参数。
-
serve() 的启动编排(serve_tinker.py):函数开头集中做前置断言——必须开启 multi-LoRA、--load 必须等于 --hf-checkpoint(训练与推理共用同一份冻结基座)、checkpoint 根目录必须可推导;随后从 HF config 取 max_position_embeddings,并在设置 --max-tokens-per-gpu 时与 trainer 的 padding 块(tensor_model_parallel_size × data_pad_size_multiplier)取小,得到 GatewayConfig.max_tokens_per_datum。启动顺序为 worker manager → object store(contribute_segment=False)→ InferenceController → TrainerController(role="actor", with_ref=False, inference_controller=None, rollout_executor=None) → 组装 GatewayConfig 与 TinkerService(MilesBackend(...));target_modules 先经 convert_target_modules_to_hf 归一化,再推导 trains_attn / trains_mlp / trains_unembed。dp_size 由 actor_num_nodes × actor_num_gpus_per_node ÷ (TP × PP × CP) 算出并注入 MilesBackend。最后 service.run() 与 uvicorn 的 server.serve() 用 asyncio.wait(..., return_when=FIRST_COMPLETED) 互相监督,任一方结束都会取消另一方,并在正常完成路径上按 inference controller → trainer 顺序 dispose。__main__ 强制 use_dynamic_global_batch_size=True 与 delay_split_train_data_by_dp=True,因为一条训练命令就是一个 batch。
-
训练路径(miles/tinker/runtime.py 的 MilesBackend):_build_train_data 把 slot datum 列表翻译成 miles 的 train_data——tokens / target_tokens / loss_masks / response_lengths / total_lengths / sample_indices / adapter_slots / dynamic_global_batch_size,并通过 DATUM_TO_BATCH_KEYS 选择性带上 loss_weights / advantages / rollout_log_probs;_pad_to_dp_multiple 在 batch 构造前用「最后一条 datum 的零掩码副本」把样本数补齐到 dp_size 的整数倍。_call_trainer 把 train_data 放进 object store(ROLLOUT_DATA_VALUE_SPEC)后以 data_ref 调用 trainer RPC,finally 中回收引用;_execute_batch 收齐各 rank 的 per_datum 结果后按 sample_index 重组,只返回调用方真实提交的 datum,显式 error 结果原样透传。slot 生命周期 RPC(load_slot / unload_slot / save_slot / export_slot)统一用 _slot_failure 取出首个非 None 的失败结果,optim_step 取 worker_results[0],trainer_dead() 转发到 TrainerController.has_errored_cell()。
-
采样路径(MilesBackend.sample / _generate_request):把 payload 编译成 SGLang /generate 请求并用 asyncio.gather 开 num_samples 份;_with_sample_seed 在调用方指定 seed 时按 index 递增,避免 N 条样本重复。stop 语义按 Tinker 定义区分处理——[] 映射为 ignore_eos=True、整数列表映射为 stop_token_ids、其余映射为 stop。LoRA 请求带上不可变的 adapter 名 lora_path,并通过 lora_backfill_paths 让引擎自行回填被驱逐的磁盘版本。_prompt_logprobs / _topk_prompt_logprobs 把 SGLang 的 meta_info 翻译成 Tinker 期望的形状,缺失位置用 NaN 和 (0, MASK_LOGPROB) 占位;_to_sequence 把 finish_reason.type == "abort" 直接转成 error,避免截断样本被客户端当成成功结果继续训练。
-
失败契约与配套改动(本 PR 无新增测试):miles/ray/train/group.py 新增 TrainerController.has_errored_cell(),让网关能感知 cell 已被 kill;miles/backends/megatron_utils/model.py 把 get_optimizer_param_scheduler 的返回类型放宽为 OptimizerParamScheduler | None,与 serve 入口没有 rollout 迭代的调用路径对齐(docstring 的 Returns 描述未同步更新);miles/tinker/server/encoding.py 在 render_result 的 sample 分支把 NaN 转成 JSON 的 null、剥掉 MASK_LOGPROB 占位、整行被掩码时输出 None;docs/advanced/lora.md 把多 LoRA 描述重写为「由 Tinker 协议承载、客户端驱动自己的 adapter slot」,并删除了过时的 Internals 清单。测试与示例全部落在栈内 #2849,本 PR 的验证叙述(checkpoint 根目录拒绝探测、slot checkpoint 续训、pre-commit)都在栈级 body 中给出。
关键文件:
miles/tinker/runtime.py(模块 网关运行时;类别 source;类型 core-logic;符号 _pad_to_dp_multiple, _build_train_data, MilesBackend, MilesBackend._execute_batch): 新增的核心运行时:MilesBackend 定义 gateway datum 到 miles train_data 的翻译契约、DP 补齐、对象存储传递训练命令、per-datum 输出重组与 router 采样,是整个 Tinker 网关的边界层。
serve_tinker.py(模块 服务入口;类别 source;类型 entrypoint;符号 serve, main): 独立 serve 入口:把 worker manager、object store、InferenceController、TrainerController、TinkerService 与 uvicorn 编排起来,并定义了服务循环与 HTTP server 的互相监督及 dispose 顺序。
miles/utils/arguments.py(模块 参数解析;类别 source;类型 configuration;符号 parse_args, get_miles_extra_args_provider, miles_validate_args, add_lora_arguments): 引入 entry 维度与 --tinker-* 参数:把 --rollout-batch-size 从 argparse 必填改为按 entry 校验,使 serve 入口可以不带 rollout 语义启动,是共享解析层的行为变化点。
miles/tinker/server/encoding.py(模块 协议编码;类别 source;类型 data-contract;符号 render_result, _tensor_json): 补完 sample 响应的 wire 编码:把 runtime 的 NaN 占位转为 JSON null,剥掉 MASK_LOGPROB 占位,保证 SDK 侧拿到合法 JSON。
miles/ray/train/group.py(模块 训练控制器;类别 source;类型 core-logic;符号 TrainerController.has_errored_cell): 新增 TrainerController.has_errored_cell(),为网关提供「训练 cell 是否已被 kill」的判定入口,是失败契约对齐的关键钩子。
miles/backends/megatron_utils/model.py(模块 模型后端;类别 source;类型 data-contract;符号 get_optimizer_param_scheduler): 把 get_optimizer_param_scheduler 的返回类型放宽为 OptimizerParamScheduler | None,与 serve 入口下没有 rollout 迭代语义的调用路径对齐。
docs/advanced/lora.md(模块 文档;类别 docs;类型 documentation): 把 multi-LoRA 章节重写为 Tinker 协议叙事(serve_tinker.py 作为服务入口、槽位机制在 lora/ 下),并删除过时的 Internals 清单,是对外说明的唯一同步文档。
关键符号:_pad_to_dp_multiple, _build_train_data, MilesBackend.init, MilesBackend.trainer_dead, MilesBackend._execute_batch, MilesBackend._call_trainer, MilesBackend.sample, MilesBackend._generate_request, MilesBackend._slot_failure, _to_sequence, _prompt_logprobs, serve, parse_args, TrainerController.has_errored_cell, render_result, get_optimizer_param_scheduler
关键源码片段
miles/tinker/runtime.py
新增的核心运行时:MilesBackend 定义 gateway datum 到 miles train_data 的翻译契约、DP 补齐、对象存储传递训练命令、per-datum 输出重组与 router 采样,是整个 Tinker 网关的边界层。
# internal datum key -> trainer batch key:Tinker datum 键到 miles 训练 batch 键的映射
DATUM_TO_BATCH_KEYS = {"weights": "loss_weights", "advantages": "advantages", "sampling_logprobs": "rollout_log_probs"}
def _pad_to_dp_multiple(slot_datums: list, dp_size: int) -> list:
"""Equalize DP shares with zero-mask datums; their outputs are dropped after the loss pass."""
# 一条训练命令必须均分到整个 DP 组,否则 worker 侧分片不齐会直接卡死
remainder = len(slot_datums) % dp_size
if remainder == 0:
return slot_datums
slot, datum = slot_datums[-1]
# 副本复用最后一条 datum 的 tokens,只把 padding 标记打开
filler = dict(datum) | {"padding": True}
return slot_datums + [(slot, filler)] * (dp_size - remainder)
def _build_train_data(slot_datums: list) -> dict:
"""Miles response_lengths select label positions here, including prompt targets."""
datums = [datum for _, datum in slot_datums]
train_data = {
"tokens": [datum["tokens"] for datum in datums],
"target_tokens": [datum["target_tokens"] for datum in datums],
# padding datum 的 loss_mask 全 0,所以它的 loss/logprob 会在重组时被丢弃
"loss_masks": [[0 if datum.get("padding") else 1] * datum["target_len"] for datum in datums],
"response_lengths": [datum["target_len"] for datum in datums],
"total_lengths": [len(datum["tokens"]) for datum in datums],
# sample_index 是跨 DP rank 重新组装 per-datum 输出的唯一键
"sample_indices": list(range(len(datums))),
# adapter_slots 让不同的 LoRA slot 共享同一次前反向
"adapter_slots": [slot for slot, _ in slot_datums],
# 一条命令就是一个 batch,绕开 miles 固定 global batch size 的切分逻辑
"dynamic_global_batch_size": len(datums),
}
# 可选键以首条 datum 为准:Tinker 的 loss_fn 决定 weights/advantages 是否存在
for datum_key, batch_key in DATUM_TO_BATCH_KEYS.items():
if datum_key in datums[0]:
train_data[batch_key] = [datum[datum_key] for datum in datums]
return train_data
class MilesBackend:
# ... __init__ / trainer_dead / load_slot / unload_slot / save_slot / export_slot 略 ...
async def _execute_batch(
self, method: str, batch_id: int, slot_datums: list, loss_fn: str, loss_fn_config: dict
) -> list[dict] | dict:
# DP 补齐必须在构造 batch 之前完成,padding 的位置决定后续 index 区间
train_data = _build_train_data(_pad_to_dp_multiple(slot_datums, self.dp_size))
train_data["loss_fn"] = loss_fn
train_data["loss_fn_config"] = loss_fn_config
worker_results = await self._call_trainer(method, batch_id, train_data)
by_index: dict[int, dict] = {}
for worker_result in worker_results:
if "error" in worker_result:
# actor 侧的显式失败原样透传给网关,由网关判定是否属于用户错误
return worker_result
for datum_output in worker_result["per_datum"]:
index = int(datum_output["sample_index"])
if index not in by_index: # 重复输出与 padding 只保留第一份
by_index[index] = {
"loss": float(datum_output["loss"]),
"logprobs": datum_output["logprobs"].tolist(),
}
# 只取调用方真实提交的 datum,尾部 padding 的 index 落在区间之外
return [by_index[index] for index in range(len(slot_datums))]
async def _call_trainer(self, method: str, batch_id: int, train_data: dict) -> list:
store = object_store.get_instance()
# 训练命令先落对象存储,worker 自己按 DP 切分并就地读取,driver 不做二次分发
data_ref = store.put(value=train_data, value_spec=ROLLOUT_DATA_VALUE_SPEC)
try:
return await getattr(self.trainer, method)(batch_id=batch_id, data_ref=data_ref)
finally:
store.remove(data_ref) # 成功与失败都要回收,否则对象存储持续增长
serve_tinker.py
独立 serve 入口:把 worker manager、object store、InferenceController、TrainerController、TinkerService 与 uvicorn 编排起来,并定义了服务循环与 HTTP server 的互相监督及 dispose 顺序。
async def serve(args):
# 前置校验集中在最前:任何一条不满足都在拉起 GPU 栈之前失败
assert args.multi_lora, "serve_tinker requires --multi-lora-n-adapters > 0"
assert args.load == args.hf_checkpoint, "Tinker trainers and engines must load the same frozen HF base"
checkpoint_root = args.tinker_checkpoint_root or (args.save and f"{args.save}/tinker")
assert checkpoint_root, "set --tinker-checkpoint-root (or --save to derive <save>/tinker)"
hf_config = load_hf_config(args.hf_checkpoint)
# 单条 datum 的上限同时受模型上下文与 trainer 的最小 padding 块约束
max_tokens_per_datum = hf_config.max_position_embeddings
if args.max_tokens_per_gpu is not None:
# The trainer pads each packed microbatch to this multiple.
pad_size = args.tensor_model_parallel_size * args.data_pad_size_multiplier
trainer_token_limit = args.max_tokens_per_gpu // pad_size * pad_size
max_tokens_per_datum = min(max_tokens_per_datum, trainer_token_limit)
assert max_tokens_per_datum > 0, "trainer token budget must fit at least one padding block"
# ... configure_logger / init_http_client / launch_worker_manager / object_store.init_instance 略 ...
trainer = TrainerController(
args=args,
role="actor",
with_ref=False,
with_opd_teacher=False,
# 采样走 SGLang router,训练侧不持有推理控制器也不参与权重更新窗口
inference_controller=None,
rollout_executor=None,
)
await trainer.init()
# target modules 先归一化成 HF 名,再据此推导 LoRA 覆盖范围
target_modules = set(convert_target_modules_to_hf(args.target_modules))
config = GatewayConfig(
base_model=args.tinker_base_model or args.hf_checkpoint,
n_slots=args.multi_lora_n_adapters,
checkpoint_root=checkpoint_root,
vocab_size=hf_config.vocab_size,
max_tokens_per_datum=max_tokens_per_datum,
lora_alpha=args.lora_alpha,
max_lora_rank=args.lora_rank,
trains_attn=bool(target_modules & {"q_proj", "k_proj", "v_proj", "o_proj"}),
trains_mlp=bool(target_modules & {"gate_proj", "up_proj", "down_proj"}),
trains_unembed="lm_head" in target_modules,
)
# ... router_url / dp_size / TinkerService(MilesBackend(trainer, router_url, dp_size=dp_size)) 略 ...
# supervise both:dispatcher 崩溃必须把 HTTP server 一起带走,
# 否则 /healthz 仍在应答,而所有训练 future 永久挂起
service_task = asyncio.create_task(service.run())
server_task = asyncio.create_task(server.serve())
try:
done, _ = await asyncio.wait({service_task, server_task}, return_when=asyncio.FIRST_COMPLETED)
for task in done:
task.result() # 在 serve() 内重新抛出子任务异常
finally:
for task in (service_task, server_task):
task.cancel()
with suppress(asyncio.CancelledError):
await task
# 顺序与 train.py 一致:先推理控制器,再 trainer;只覆盖正常完成路径
await inference_controller.dispose()
await trainer.dispose()
if __name__ == "__main__":
args = parse_args(entry="serve")
# 命令一次只提交一个 work unit,其大小就是 batch size
args.use_dynamic_global_batch_size = True
args.delay_split_train_data_by_dp = True
asyncio.run(serve(args))
miles/tinker/server/encoding.py
补完 sample 响应的 wire 编码:把 runtime 的 NaN 占位转为 JSON null,剥掉 MASK_LOGPROB 占位,保证 SDK 侧拿到合法 JSON。
if op == "sample":
rendered = {"type": "sample", "sequences": result["sequences"]}
if result.get("prompt_logprobs") is not None:
# runtime 用 NaN 表示「该位置没有可评分的 token」;JSON 不允许 NaN,转成 null
rendered["prompt_logprobs"] = [
None if math.isnan(logprob) else logprob for logprob in result["prompt_logprobs"]
]
if result.get("topk_prompt_logprobs") is not None:
topk = result["topk_prompt_logprobs"]
# runtime 用 (0, MASK_LOGPROB) 填充不足 k 的位置,编码层负责剥离占位符
rendered["topk_prompt_logprobs"] = [
[
(token_id, logprob)
for token_id, logprob in zip(ids, probs, strict=True)
if (token_id, logprob) != (0, MASK_LOGPROB)
]
or None # 整行都被掩码时输出 null,而不是空列表
for ids, probs in zip(topk["token_ids"], topk["logprobs"], strict=True)
]
return rendered
评论区精华
本轮 review 由 nanjiangwill 给出「三项正确性/可用性关注 + 两项显式非阻塞的生命周期建议」,作者逐条以 “Addressed:” 回复,最终由 yushengsu-thu approve。
1) 训练 cell 被杀与服务失败语义错位(最重要):reviewer 指出 TrainerController._execute_slots() 调用 TrainerCell.execute() 时用的是默认 kill_on_failure=True,「any actor exception marks the only trainer cell errored and kills its workers. TinkerService instead treats batch failures as recoverable and calls zero_grads() before continuing」,结果是清理逻辑对着一个已死的 cell 执行、派发随之停摆;创建/加载失败同样会让服务「活着但没有可用的 trainer」。结论是补齐契约:显式 actor 失败结果透传给网关,未知训练失败沿用 TrainerCell 边界并由 supervisor 终止当前单 cell 服务,CUDA/NCCL 类故障保持 cell-fatal。
2) abort finish reason 被吞成成功样本:reviewer 指出 _to_sequence() 把所有非 length 的 finish reason 都映射为 stop,「SGLang returns type=\"abort\", and miles’ normal rollout path maps that to Sample.Status.ABORTED; here it becomes a successful partial sample that the client can train on」,建议对 abort 抛错。最终实现改为返回 {"error": "the engine aborted this sample; resubmit the request"}。
3) 服务循环与 HTTP server 的监督:service.run() 与 uvicorn 现在一起被监督,「completion or failure of either task tears down the other」,避免 dispatcher 已崩溃而 /healthz 仍在应答、所有训练 future 永久挂起。
4) 非阻塞项一:把 checkpoint_root 的计算与校验提前到 launch_worker_manager() 之前,避免配置非法时先拉起 worker 与 GPU 栈再断言失败——最终 serve() 开头的断言顺序已按要求前置。
5) 非阻塞项二:建议在 finally 中 dispose trainer 与 inference_controller(停掉 watcher、ticker 与推理 server 客户端)。这一条被有意保留:PR body 明确写「Controller lifecycle: match train.py by disposing the inference controller, then the trainer, after normal completion of the serving tasks. No AsyncExitStack or initialization/exception cleanup guarantees are introduced」,即只覆盖正常完成路径。
- 训练 cell 被 kill 与 Tinker 服务「可恢复失败」语义错位 (correctness): 按 reviewer 建议补齐契约:显式 actor 失败结果经
_slot_failure / error 字段透传给网关,未知训练失败沿用 TrainerCell 边界并由 supervisor 终止当前单 cell 服务,CUDA/NCCL 类故障保持 cell-fatal;实现上新增 TrainerController.has_errored_cell() 与 MilesBackend.trainer_dead() 作为判定入口。
abort finish reason 被映射为成功样本 (correctness): 改为对 abort 返回 {"error": "the engine aborted this sample; resubmit the request"},让请求在服务层结算为失败而不是静默返回截断序列。
- service 循环与 uvicorn 的互相监督 (design):
service.run() 与 server.serve() 现在通过 asyncio.wait(..., return_when=FIRST_COMPLETED) 共同被监督,并在 finally 中取消、等待两个任务。
- checkpoint_root 校验与资源初始化顺序 (design): 已采纳:
serve() 开头的断言顺序前置,checkpoint 根目录与 --load == --hf-checkpoint 的检查都在 worker 初始化之前执行。
- 控制器 dispose 是否放进 finally (design): 有意保留现状:PR body 明确说明「Controller lifecycle: match
train.py by disposing the inference controller, then the trainer, after normal completion of the serving tasks. No AsyncExitStack or initialization/exception cleanup guarantees are introduced」,即只覆盖正常完成路径,异常路径的清理由进程退出兜底。
风险与影响
- 风险:可用性:单 cell 服务的故障半径。
serve_tinker.py 只为 actor 建一份 cell,而 TrainerCell.execute() 默认 kill_on_failure=True;一次 CUDA/NCCL 级故障会经 trainer_dead() → has_errored_cell() 终止整个网关。多租户并发下,一个租户触发的训练故障会连带中断其他租户。
正确性:_build_train_data 的同构假设。可选键只用首条 datum 探测(if datum_key in datums[0]):第一条没有 weights 而后继 datum 有时,这些键会被静默丢弃;反之则抛 KeyError 直接变成 500,而不是 UserInputError 之类的结构化错误。
正确性:结果重组缺少缺项检查。_execute_batch 结尾是 [by_index[index] for index in range(len(slot_datums))],若某个 rank 结果缺失或 sample_index 冲突,会抛 KeyError 而不是返回 {"error": ...};同一 sample_index 的重复输出也只保留第一份,缺少显式日志。
性能:DP 补齐的无效算力。_pad_to_dp_multiple 最多复制 dp_size - 1 条样本,且复用的是最后一条 datum 的 token 长度;DP 越大、单命令批量越小时,padding 带来的无效 forward/backward 比例越高。
兼容性:参数校验时机后移。--rollout-batch-size 不再是 argparse 必填。miles_validate_args 中 if args.num_steps_per_rollout is not None: 一段位于 entry 判断之前,serve 入口若误传 --num-steps-per-rollout 会以 None * ... 的 TypeError 失败,而不是可读的断言信息。
契约分散:logprob wire 格式。NaN 与 MASK_LOGPROB 的占位约定同时存在于 miles/tinker/runtime.py 与 miles/tinker/server/encoding.py,走 protobuf(proto_codec)的路径需要各自实现同样语义,存在不一致风险。
启动门槛。assert args.load == args.hf_checkpoint 要求 serve 只能从冻结 HF 基座起,不能像 train 那样从 miles checkpoint 恢复;按训练习惯传 --load 的用户会在启动即失败。
测试覆盖。本 PR 未新增测试文件,MilesBackend 的翻译逻辑与 serve() 的监督/生命周期行为只能通过 #2849 的网关测试与 e2e 间接覆盖,栈级 body 也标注「Fresh hosted CPU/NVIDIA CI is pending」。
- 影响:对用户/调用方:Tinker SDK 客户端第一次可以真正连到一个自带训练与采样的 miles 部署——
serve_tinker.py 是唯一入口,端口(--tinker-server-port)、对外基座名(--tinker-base-model)、checkpoint 根目录(--tinker-checkpoint-root)都是显式契约,tinker:// 路径语义由此确定。
对系统:新增一条与 train.py 平行的 serve 入口,通过 args.entry 隔离两者的参数校验;serve() 只创建 actor、with_ref=False、inference_controller=None,采样经 router 走磁盘回填,不参与权重更新窗口,因此不会改变既有训练链路的权重同步路径。
对共享层:miles/utils/arguments.py 是所有入口共用的参数解析,--rollout-batch-size 由 required=True 改为可选属于全局行为变化(主要影响报错时机与文案);miles/ray/train/group.py 新增方法、miles/backends/megatron_utils/model.py 放宽注解都是向后兼容的增量改动。
对团队/演进:本 PR 完成 7 篇栈的第 6 环,MilesBackend 定义的 datum↔train_data 契约将成为后续所有 Tinker 协议扩展的边界;第 7 环 #2849 负责示例、SDK 客户端与测试套件,本 PR 的复用点(测试文件、pr-2849 Docker 镜像)都指向它。
- 风险标记:核心路径变更, 失败语义耦合(kill_on_failure), 缺少本 PR 测试覆盖, 参数校验时机后移, DP 补齐带来额外算力, wire 契约分散在两处
关联脉络
- PR #2849 [multi-lora v2 7/7] Tinker gateway example and tests: 直接叠加在本 PR 之上的第 7 篇:提供
serve_qwen3_30b_a3b_tinker.py 启动器、SDK 客户端与网关测试套件;PR body 指出 checkpoint 回归测试挂在该 PR 的测试文件里,运行环境也复用 pr-2849 Docker 镜像,本 PR 的功能验证主要依赖它。
- PR #2844 (标题未在上下文中提供): PR body 明确写「Stacked on #2844」,本 PR 直接构建在其之上,属于同一 multi-lora v2 栈的相邻切片。
- PR #2840 (标题未在上下文中提供): PR body 说明「Per-slot optimizer ownership is established in #2840」,本 PR 的
MilesBackend.optim_step 与 slot 生命周期 RPC 依赖其建立的 per-slot 优化器归属语义。
- PR #3198 (标题未在上下文中提供): 整个栈的最终集成点:PR body 多次以「the final #3198 tree exactly matches the reviewed implementation」「broad GPU selection remains on #3198」指代它,说明本 PR 的最终形态与 #3198 的评审树一致。
参与讨论