执行摘要
- 一句话:删除 multi-LoRA v1 实现,仅保留槽位原语
- 推荐动作:值得精读,但重点不是 5000 行删除本身,而是删除与保留的边界如何划定:建议先读
miles/backends/megatron_utils/lora/slots.py(保留原语的新家,含跨 rank shard 写入者选举与 expert-axis-safe 的 rank 切分)与 miles/utils/multi_lora.py(收缩后的公共工具面),再回看 miles/ray/multi_lora/registry.py、backend.py 被删掉的注册期校验与状态机,逐条确认 v2(#2844 的 miles/tinker/core/scheduler.py/service.py、#2840 的 slot actor)是否有等价实现。三个最值得关注的设计决策:一是槽位原语与控制面彻底解耦(docstring 里显式声明不掺控制面/数据面逻辑);二是注册期 fail-fast 校验(data 路径、reward 配置、batch 形状整除)整体消失后由谁承担;三是 per-slot LR 调度与 adapter YAML 契约被删,v2 是否真的不再需要 per-adapter 调度。阅读时注意 head 只有一个 squash commit,MoE 专家路径的恢复要直接核对 head_sha 958e1ca 的文件内容。
功能与动机
按 PR body 的表述,动因是 v2 要转向以 Tinker 命令执行器为中心的槽位化多租户形态,而 v1 是另一套完整体系:“Remove the dataset-driven multi-LoRA v1 driver, per-adapter data sources and scheduling, adapter controller, and legacy checkpoint flow.” 若两套控制面/数据面并存,v2 的改造无法收敛,所以选择一次性删除;“Retain the slot primitives under miles/backends/megatron_utils/lora/ ... The Tinker command executor builds on these primitives in #2840” 划定了保留边界。作者在 issue 评论中进一步说明保留清单被修正过:MoE expert adapter 的 build-time 部分(#1794 的 targets_expert_leaves、permute-fusion disable、post-finalize MoE geometry 校验)最初在删除范围内,最终“restored in the trailing commit”,因为专家路径是跨版本复用的能力而非 v1 特有逻辑。
实现拆解
-
删除 v1 启动入口与示例脚本:train_multi_lora_async.py(-109,含 main、_is_empty_batch_timeout)、examples/multi_lora/run_multi_lora.py(-204,含 ScriptArgs、prepare/train/full_train/serve 四个 typer 子命令)、examples/multi_lora/service_smoke.py(-170,含 ServiceClient、wait_for_step、register_when_allowed)全部移除。原因是这套入口拼装的 --multi-lora-adapter/--multi-lora-disable-service-mode/--multi-lora-api-port 参数组合绑定了 v1 控制面;影响是原先的 multi-LoRA 使用路径在 v2 落地前没有替代入口,必须按 PR 栈顺序合入。
-
拆除控制面(Ray actor + HTTP + registry + backend):miles/ray/multi_lora/registry.py(-252)删除 AdapterState/AdapterRecord/AdapterRegistry 的生命周期状态机与 mark_batch_trained 记账逻辑;miles/ray/multi_lora/backend.py(-189)删除注册期校验 resolve_adapter_config(rank/alpha 上限、rollout_batch_size 与 min_groups_per_dp_split 的整除约束、data 路径存在性、reward 配置非空、save 目录冲突)与 abort_adapter_requests;miles/ray/multi_lora/http_server.py(-129)删除 /adapter_runs REST 路由与 RegisterAdapterRequest;miles/ray/multi_lora/controller.py(-123)删除 MultiLoRAController 与 AdaptersCache。这些能力在 v2 中由 miles/tinker/core/scheduler.py、miles/tinker/core/service.py(#2844)承接,槽位本身的簿记不再由控制面持有。
-
拆除数据面(per-adapter 数据源与完全异步 rollout):miles/rollout/multi_lora/data_source.py(-137)删除 MultiLoRAAsyncDataSource.reconcile/create_source/get_samples 的轮询取样本、resolve_num_step 的 num_epoch→num_step 推导;miles/rollout/multi_lora/async_rollout.py(-584,本次最大单文件删除)删除 GroupBuffer(drop_foreign/drop_stale)、remaining_groups 的 batch 封顶、process_group 的 slot_version/registration_id 打戳、MultiLoRAWorkerMetrics 的 per-adapter step 统计。删除原因是“按数据集驱动多租户并发训练”这一整套语义由 v2 重新设计;风险在于权重版本 staleness 与同名重注册的样本隔离这两条不变量需要在 v2 里等价重建。
-
保留并搬迁槽位原语:新增 miles/backends/megatron_utils/lora/slots.py(+140),承接原 miles/backends/megatron_utils/multi_lora_utils.py(-490)中被判定为跨版本复用的部分:create_multi_lora_instance、megatron_shard_name、adapter_shard_topology、zero_optimizer_state_for_adapter、slice_lora_to_rank;文件 docstring 明确“Kept deliberately free of any control-plane or data-plane logic”。miles/utils/multi_lora.py 收缩为 +17/-112,保留 AdapterSpec、EmptyBatchTimeoutError、define_new_adapter_metrics、slot_lora_name、make_rid、parse_adapter、min_groups_per_dp_split,即路由命名、RID 拼接与 per-adapter 指标命名等仍然共用的工具。被判定为 v1 特有的 miles/utils/adapter_config.py(-93,AdapterRunConfig/AdapterRun/parse_adapter_run_yaml)与 miles/backends/megatron_utils/multi_lora_scheduler.py(-88,per-slot LR/WD 调度 build_slot_scheduler/step_slot_schedulers)则整体删除,等于宣告 per-adapter YAML 配置契约与 per-adapter 独立学习率调度不在 v2 计划内。
-
测试、文档与校验配套:同步删除 6 个以上 fast 测试文件,包括 tests/fast/ray/multi_lora/test_controller_backend.py(-392)、tests/fast/ray/rollout/test_multi_lora_batch_collection.py(-313)、tests/fast/ray/multi_lora/test_controller_http.py(-239)、tests/fast/test_train_multi_lora_async.py(-189)、tests/fast/backends/megatron_utils/test_multi_lora_scheduler.py(-114)、tests/fast/ray/rollout/test_multi_lora_train_data.py(-106),它们与源码同批移除以避免 import error;PR body 声明 multi-LoRA 文档同步更新,且保留了 prefill 批处理断言(即 async_rollout.py 引用的 recompute_samples_rollout_logprobs_via_prefill 一线),说明该断言并非 v1 专用。合入 main 时额外做了两处冲突对齐:保留 rollout session-metrics 的 import,移除已废弃的 AdapterRef 引用(与 #2598 的 metrics 改动相关)。本 PR 明确“adds no test cases”,验证仅为 pre-commit run --all-files 与组件级 git diff --check,限制与后续验证记录在 #2846。
关键文件:
miles/backends/megatron_utils/lora/slots.py(模块 槽位管理;类别 source;类型 core-logic;符号 create_multi_lora_instance, megatron_shard_name, adapter_shard_topology, zero_optimizer_state_for_adapter): 本 PR 唯一新增文件,是被保留下来、供 v2 Tinker 命令执行器复用的槽位原语集合(模型插桩、shard 寻址、per-slot 优化器状态清理、rank 轴切分)。它定义了 v1 与 v2 的能力边界,是理解整个删除清单的钥匙。
miles/backends/megatron_utils/multi_lora_utils.py(模块 槽位管理;类别 source;类型 deletion;符号 create_multi_lora_instance, megatron_shard_name, adapter_shard_topology, all_megatron_checkpoints_exist): 被删除的 490 行 v1 宿主文件:其中的槽位原语被搬进 lora/slots.py 保留,而 save_multi_lora_checkpoints(双格式落盘)与 _register_adapter(按 adapter 加载 checkpoint)属于 v1 checkpoint 流程,被 #2842/#2843 的新方案取代。对照这份文件能看清“哪些缩进到了 lora/ 目录、哪些彻底消失”。
miles/rollout/multi_lora/async_rollout.py(模块 异步采样;类别 source;类型 deletion;符号 iter_group_samples, first_sample, group_adapter_name, group_sample_count): 本次最大单文件删除(-584):完全异步 rollout 的 per-adapter 缓冲、staleness 过滤、注册隔离与 step 统计全部消失。这里的 drop_stale/drop_foreign/remaining_groups 是 v1 数据面的核心不变量,也是判断 v2 是否等价重建的主要依据。
miles/ray/multi_lora/registry.py(模块 租户注册;类别 source;类型 deletion;符号 AdapterState, AdapterRecord, AdapterRegistry, register): 被删除的控制面核心(-252):AdapterState/AdapterRecord 定义租户生命周期与 step 记账,mark_batch_trained 是唯一推进 accumulated_groups/step 的位置。v2 把这套状态机交给 Tinker 网关会话/调度器,理解它是理解 #2844 的前提。
miles/ray/multi_lora/backend.py(模块 控制面;类别 source;类型 deletion;符号 MultiLoRABackend, init, close, validate_adapter): 被删除的注册期校验与引擎侧 abort(-189)。resolve_adapter_config 里集中了所有批量形状与前置条件校验(rank 上限、rollout_batch_size 整除、data 路径存在、reward 配置非空、save 目录冲突),删除后 v2 是否有等价的 fail-fast 校验是主要风险点。
miles/utils/multi_lora.py(模块 公共工具;类别 source;类型 refactor;符号 AdapterSpec, EmptyBatchTimeoutError, define_new_adapter_metrics, slot_lora_name): 保留面在此收缩(+17/-112):AdapterSpec、EmptyBatchTimeoutError、slot_lora_name、make_rid、parse_adapter、min_groups_per_dp_split、define_new_adapter_metrics 留下来成为 v2 的公共契约面,其余 v1 专用工具被清除。
miles/rollout/multi_lora/data_source.py(模块 数据源;类别 source;类型 deletion;符号 fetch_snapshot, sampleable, MultiLoRAAsyncDataSource, reconcile): 被删除的数据面调度(-137):轮询取样本、按 adapter 建 RolloutDataSource、num_epoch→num_step 推导。它决定了 v1 的“数据集驱动多租户”语义,删除意味着 v2 不再以数据源轮询驱动 adapter 训练。
miles/ray/multi_lora/controller.py(模块 控制面;类别 source;类型 deletion;符号 get_multi_lora_controller, AdaptersCache, get_snapshot, get_all): 被删除的 Ray 控制面 actor 与 AdaptersCache(-123),是 v1 中 rollout 侧读取 adapter 快照的唯一入口(async_rollout 通过它打 slot_version/registration_id 戳)。
miles/ray/multi_lora/http_server.py(模块 控制面;类别 source;类型 deletion;符号 RegisterAdapterRequest, MultiLoRAHTTPServer, actual_api_port, create_app): 被删除的控制面 REST 层(-129):/adapter_runs 注册/查询/注销路由与异常映射。它是 v1 对外最显式的接口面,也是外部集成最直接的 breaking 点。
miles/backends/megatron_utils/multi_lora_scheduler.py(模块 调度器;类别 source;类型 deletion;符号 _SlotParamGroups, build_slot_scheduler, install_slot_scheduler, drop_slot_scheduler): 被删除的 per-slot LR/WD 调度(-88):每个 adapter 一条 OptimizerParamScheduler,按各自已训样本数定位。删除等于宣告 v2 不再保留 per-adapter 独立学习率行程,是需要产品/训练侧确认的决策。
miles/utils/adapter_config.py(模块 配置契约;类别 source;类型 data-contract;符号 AdapterRunConfig, adapter_global_batch_size, AdapterRun, parse_adapter_run_yaml): 被删除的配置契约(-93):AdapterRunConfig/AdapterRun/parse_adapter_run_yaml 是 adapter YAML 与 HTTP 注册的公共数据模型,删除后 add 与 API 两侧同时失去类型基础,说明 v2 不复用该 YAML 契约。
train_multi_lora_async.py(模块 启动入口;类别 source;类型 entrypoint;符号 _is_empty_batch_timeout, main): 被删除的仓库根入口(-109):v1 multi-LoRA 训练的启动脚本,含空批次超时判定。它是最直接的 breaking 点——从此 main 上不存在 v1 训练入口。
tests/fast/ray/multi_lora/test_controller_backend.py(模块 测试用例;类别 test;类型 test-coverage;符号 make_args, make_backend, make_config, register_and_promote): 最大的被删测试(-392),覆盖 RID 往返、注册后 PENDING→ACTIVE 提升、snapshot 词表、slot 版本跨复用单调性等不变量。作为删除清单中测试的代表,它说明 v1 的回归保护被整体移除,需由 v2 重建。
关键符号:create_multi_lora_instance, adapter_shard_topology, megatron_shard_name, zero_optimizer_state_for_adapter, slice_lora_to_rank, AdapterRegistry.mark_batch_trained, MultiLoRABackend.resolve_adapter_config, MultiLoRAAsyncDataSource.get_samples, GroupBuffer.drop_stale, GroupBuffer.drop_foreign, remaining_groups, build_slot_scheduler, min_groups_per_dp_split
关键源码片段
miles/backends/megatron_utils/lora/slots.py
本 PR 唯一新增文件,是被保留下来、供 v2 Tinker 命令执行器复用的槽位原语集合(模型插桩、shard 寻址、per-slot 优化器状态清理、rank 轴切分)。它定义了 v1 与 v2 的能力边界,是理解整个删除清单的钥匙。
miles/backends/megatron_utils/lora/slots.py(本 PR 新增):v1 保留下来、由 v2 复用的槽位原语。
# 模块 docstring 就写明了设计边界:只放模型插桩、shard 寻址与 per-slot 优化器状态清理,
# 不掺任何控制面(registry / HTTP)或数据面(data source / rollout)逻辑,
# 因为 v2 会把上层整个换成 Tinker 命令执行器(#2840)。
# 整个 run 期间拓扑固定,模块级缓存住一次 gloo all-gather 的结果。
_shard_topology: tuple[bool, tuple[tuple[int, int, int], ...]] | None = None
def megatron_shard_name(tp_rank: int, pp_rank: int, ep_rank: int, ep_size: int) -> str:
"""一个 (tp, pp, ep) 坐标对应的 adapter shard 文件名。
ep_size == 1 时省略 _ep 后缀,专家并行开启之前写下的老 checkpoint 因此仍可加载。
"""
name = f"adapter_megatron_tp{tp_rank}_pp{pp_rank}"
if ep_size > 1:
name += f"_ep{ep_rank}"
return name + ".pt"
def adapter_shard_topology() -> tuple[bool, tuple[tuple[int, int, int], ...]]:
"""返回 (本 rank 是否负责写自己的 shard, 实际存在的 (tp, pp, ep) 坐标集合)。
同一次 all-gather 顺带解决“谁写盘”:同一个坐标可能落在多个 DP rank 上,
只选 rank 号最小的进程写,避免多个进程并发写同一个 shard 文件。
"""
global _shard_topology
if _shard_topology is not None:
return _shard_topology
parallel_state = get_parallel_state()
coords = (parallel_state.tp.rank, parallel_state.pp.rank, parallel_state.ep.rank)
if not dist.is_initialized():
# 未初始化分布式时退化为“自己写自己的坐标”,方便单卡调试。
_shard_topology = (True, (coords,))
return _shard_topology
current_rank = dist.get_rank()
group = get_gloo_group()
gathered: list[object] = [None] * dist.get_world_size(group=group)
dist.all_gather_object(gathered, (coords, current_rank), group=group)
is_writer = current_rank == min(rank for entry_coords, rank in gathered if entry_coords == coords)
_shard_topology = (is_writer, tuple(sorted({entry_coords for entry_coords, _ in gathered})))
return _shard_topology
def zero_optimizer_state_for_adapter(optimizer, model, idx: int) -> None:
"""清理某个 slot 退役后的优化器状态,让下一个租户从零开始(bias correction 重新计时)。
v1 靠“复用同一个优化器 + 清零状态”实现 slot 复用;v2 改由每租户独立优化器承担
(#2840 的 miles/backends/megatron_utils/lora/optimizer.py),所以这段逻辑虽然保留,
但不再是 slot 复用的唯一保障。
"""
from megatron.bridge.peft.multi_lora_layers import MultiLoRALinear, _iter_multi_lora_modules
target_main_params = set()
for module in _iter_multi_lora_modules(model):
if not isinstance(module, MultiLoRALinear):
continue
adapter = module.adapters[idx]
for param in adapter.parameters():
# distributed optimizer 会把权重挂到 main_param 上,要按 main_param 的 id 匹配
main = getattr(param, "main_param", None)
target_main_params.add(id(main if main is not None else param))
chained = getattr(optimizer, "chained_optimizers", [optimizer])
for chained_optimizer in chained:
inner = getattr(chained_optimizer, "optimizer", chained_optimizer)
if inner is None:
continue
# TE/apex FusedAdam 的 Adam step 挂在 param group 上而不是单个参数上,
# 所以要按 group 上打的 miles_multi_lora_slot 标记重置。
for group in inner.param_groups:
if group.get("miles_multi_lora_slot") == idx and "step" in group:
if isinstance(group["step"], torch.Tensor):
group["step"].zero_()
else:
group["step"] = 0
for param, state in inner.state.items():
if id(param) not in target_main_params:
continue
if "exp_avg" in state:
state["exp_avg"].zero_()
if "exp_avg_sq" in state:
state["exp_avg_sq"].zero_()
# 下一个租户的 bias correction 从这里重新开始
if "step" in state:
if isinstance(state["step"], torch.Tensor):
state["step"].zero_()
else:
state["step"] = 0
miles/rollout/multi_lora/async_rollout.py
本次最大单文件删除(-584):完全异步 rollout 的 per-adapter 缓冲、staleness 过滤、注册隔离与 step 统计全部消失。这里的 drop_stale/drop_foreign/remaining_groups 是 v1 数据面的核心不变量,也是判断 v2 是否等价重建的主要依据。
miles/rollout/multi_lora/async_rollout.py(本 PR 整文件删除):v1 数据面的两条关键不变量,v2 需要确认是否等价覆盖。
# 后台生产者线程按 adapter 往有界 FIFO 里塞 group,训练侧轮询取批;
# 训练完全停摆时靠这个上限兜底内存,与 fully_async 的 queue.Queue(maxsize=1000) 同一约定。
MAX_BUFFERED_GROUPS = 1000
EMPTY_BATCH_TIMEOUT_S = 30.0
class GroupBuffer:
"""单个 adapter 的已完成 prompt group FIFO;有界,满了丢最老的。"""
def __init__(self) -> None:
self._groups: deque[Group] = deque(maxlen=MAX_BUFFERED_GROUPS)
def get(self, n_groups: int) -> list[Group]:
"""取走最老的 n 个 group(queue.Queue 风格的接口)。"""
return [self._groups.popleft() for _ in range(n_groups)]
def drop_foreign(self, registration_id: str) -> int:
"""丢弃被同名 adapter 的另一次注册打过标记的 group。
旧租户在途的生成可能在 buffer 被重置后才落地,如果不清掉,新租户会拿到别人的样本;
提交采样时还看不到 adapter 视图、因此没打标记的 group 则保留。
"""
if not self._groups:
return 0
kept: deque[Group] = deque(maxlen=MAX_BUFFERED_GROUPS)
dropped = 0
for group in self._groups:
stamped = first_sample(group).metadata.get("registration_id")
if stamped is not None and stamped != registration_id:
dropped += 1
else:
kept.append(group)
self._groups = kept
return dropped
def drop_stale(self, current_version: int, max_staleness: int | None) -> list[int]:
"""丢弃生成时权重版本过旧的 group,返回各自 staleness 用于打点。
v1 用 --max-weight-staleness 控制 on-policy 程度:slot_version 在提交采样时打戳,
被丢的样本先 reset_for_retry() 再计数。
"""
if max_staleness is None or not self._groups:
return []
kept: deque[Group] = deque(maxlen=MAX_BUFFERED_GROUPS)
dropped: list[int] = []
for group in self._groups:
stamped = first_sample(group).metadata.get("slot_version")
staleness = current_version - stamped if stamped is not None else 0
if stamped is not None and staleness > max_staleness:
for sample in iter_group_samples(group):
sample.reset_for_retry()
dropped.append(staleness)
else:
kept.append(group)
self._groups = kept
return dropped
def remaining_groups(adapter) -> int:
"""该 adapter 还差多少 group 才能凑满一个 batch。
取批时用它封顶,保证不会为了凑某个 adapter 的 batch 而多拿别人的 group,
从而让每个 adapter 的 step 与 rollout_batch_size 严格对齐。
"""
remaining = adapter.config.rollout_batch_size - adapter.accumulated_groups
assert remaining > 0, (
f"adapter '{adapter.name}' accumulated_groups={adapter.accumulated_groups} >= "
f"rollout_batch_size={adapter.config.rollout_batch_size}; batch accounting drifted"
)
return remaining
miles/ray/multi_lora/registry.py
被删除的控制面核心(-252):AdapterState/AdapterRecord 定义租户生命周期与 step 记账,mark_batch_trained 是唯一推进 accumulated_groups/step 的位置。v2 把这套状态机交给 Tinker 网关会话/调度器,理解它是理解 #2844 的前提。
miles/ray/multi_lora/registry.py(本 PR 整文件删除):v1 租户生命周期与训练记账的核心状态机,v2 由 Tinker 网关重建。
# v1 的租户生命周期:PENDING -> ACTIVE -> RETIRING -> CLEANUP -> COMPLETED。
# slot 会被复用,但 slot_versions 永不重置,保证 (slot, version) 组合不重复出现;
# v2 把这套生命周期挪到 Tinker 网关(#2844 的 miles/tinker/core/scheduler.py 与 service.py),
# 槽位本身仍由 miles/backends/megatron_utils/lora/slots.py 维护。
class AdapterState(str, Enum):
PENDING = "PENDING" # 已注册但权重还没推上去
ACTIVE = "ACTIVE" # 权重就位,可被采样
RETIRING = "RETIRING" # 已请求下线,但继续服务在途请求
CLEANUP = "CLEANUP" # 等待 abort 完成,之后才能释放 slot
COMPLETED = "COMPLETED" # slot 已释放,记录只留作查询
@dataclass
class AdapterRecord:
name: str
slot: int
config: Any
step: int = 0
# num_step 是相对 start_step 计的,checkpoint 续训时把起点 step 存下来
start_step: int = 0
# 已提交训练的 prompt group 数;只有 mark_batch_trained(train 成功之后)才推进,
# 因此 train 调用失败重试不会污染计数器
accumulated_groups: int = 0
state: AdapterState = AdapterState.PENDING
# 每次注册都是新租户:同名再注册时,rollout 侧旧租户留下的状态必须按这个 id 丢弃
registration_id: str = field(default_factory=lambda: uuid.uuid4().hex)
def mark_batch_trained(self, rollout_id: int) -> list[str]:
"""结算一个已训练 batch 的 group 数并触发 step;返回本批完成 step 的 adapter。
这里是唯一推进 accumulated_groups / step 的地方,所以 train 失败或重试时
registry 的计数保持不动,不会出现“训了但没记账”或“记了但没训”的漂移。
"""
record_entry = self.batch_records.pop(rollout_id, None)
if record_entry is None:
return []
stepped = []
reached_num_step = []
for name, n_groups in record_entry["groups"].items():
record = self.records.get(name)
if record is None or record.state not in (
AdapterState.ACTIVE,
AdapterState.RETIRING,
AdapterState.CLEANUP,
):
continue
record.accumulated_groups += n_groups
if name in record_entry["step_names"]:
# 收集侧按 adapter 的剩余 group 数封顶,正常应正好等于 rollout_batch_size
target = record.config.rollout_batch_size
if record.accumulated_groups != target:
logger.warning(
f"Adapter '{name}' stepped with accumulated_groups={record.accumulated_groups} "
f"!= rollout_batch_size={target}; adapter batch accounting drifted"
)
record.step += 1
record.accumulated_groups = 0
stepped.append(name)
# num_step 到点即自动注销;用 step - start_step 支持从 checkpoint 续训
if (
getattr(record.config, "num_step", None) is not None
and record.state is AdapterState.ACTIVE
and (record.step - record.start_step) >= record.config.num_step
):
reached_num_step.append(name)
for name in reached_num_step:
self.deregister(name)
return stepped
评论区精华
本 PR 的评审记录非常薄:claude[bot] 只留下仓库级 Code Review 模板评论,yushengsu-thu 给出 APPROVED 且正文为空,没有任何行内 review comment,因此实质讨论发生在 PR 之外的 issue 评论里。最有价值的一处是作者对保留清单的自我修正:MoE expert adapter 的 build-time 部分(#1794 的 targets_expert_leaves、permute-fusion disable、post-finalize MoE geometry 校验)最初被删,最终在 trailing commit 中恢复保留,理由是该路径属于专家适配器的通用几何约束而非 v1 控制面逻辑;其余专家路径(EP-aware shard 拓扑、expert-axis-safe 的 rank 切片)本来就在保留清单。第二处是 PR body 明示的覆盖缺口:“This update adds no test cases. Current validation and limitations are recorded on #2846.”,即删除 5000 行实现与配套测试后,回归保护完全依赖后续 PR 重建。注意提交列表只有一个 squash 后的 commit(head 958e1ca),说明“trailing commit”以 amend/squash 形式进入 head,审查时应以 head_sha 为准而非提交历史。
- MoE 专家适配器(#1794)的 build-time 逻辑是删还是留 (design): 最终采用保留方案,在 trailing commit 中恢复;但提交列表只有 1 个 squash commit,需以 head_sha 958e1ca 的文件内容确认恢复完整。
- 删除范围与 v2 落地时序的兼容性风险 (question): 以 PR 栈顺序合入作为隐式约定;该风险未被显式评审,属于需要持续跟踪项。
- 本 PR 不新增测试,验证依赖后续 PR 重建 (testing): 已知限制,验证与限制记录在 #2846;租户生命周期、slot 版本单调性、batch 记账漂移告警等不变量暂时失去回归保护。
- 评审过程本身缺少实质交锋 (other): 该 PR 的实质决策讨论发生在 issue 评论与 #2846,而非 PR 评论线程,后续同类清理型 PR 建议在描述里补充保留/删除清单对照表。
风险与影响
- 风险:1)兼容性 breaking,且本 PR 单独合入时功能缺位:
train_multi_lora_async.py、examples/multi_lora/run_multi_lora.py、/adapter_runs HTTP API、miles/utils/adapter_config.py 的 AdapterRunConfig 全部消失,任何依赖 v1 入口或 --multi-lora-adapter YAML 的外部脚本/流水线会直接失败;v2 替代实现在 #2840-#2849,因此必须严格按栈序合入,main 在中间窗口内没有可用的 multi-LoRA 路径。2)删除的验证覆盖形成的回归盲区:test_controller_backend.py、test_multi_lora_batch_collection.py 覆盖的租户生命周期、slot_versions 单调性、batch 记账漂移告警(accumulated_groups != rollout_batch_size)等不变量随测试一并删除,v2 若不重建等价断言,这类漂移会静默发生。3)数据面不变量的迁移风险:GroupBuffer.drop_foreign 用 registration_id 隔离同名重注册的残留样本、drop_stale 用 slot_version 与 --max-weight-staleness 控制 on-policy 程度,这两条保证在 v2 中是否等价存在,是后续 review 的关键检查项。4)per-slot 调度与配置契约被放弃:multi_lora_scheduler.py 的 per-adapter LR/WD 调度与 adapter_config.py 的 YAML 契约删除后不可恢复,若 v2 仍需 per-adapter 学习率行程,需要重新设计而非简单搬运。5)import 残留:miles/utils/multi_lora.py 保留面收窄、AdapterRef 被移除,任何未一并清理的调用方(包括与 #2598 相关的 rollout metrics / sample codec 路径)会 import error;本 PR “至少 22 个测试文件与源码模块存在名称交集”,交集之外的引用需要靠 CI 兜底。6)MoE 专家路径的恢复完整性:issue 评论说明 build-time 三处逻辑是“trailing commit”补回来的,需确认恢复的不是残缺版本(例如几何校验只恢复了调用而没恢复常量或分支),否则 MoE 适配器会静默失去构建期保护。
- 影响:对用户:仍在用 v1 路径的用户(
run_multi_lora.py、service_smoke.py、adapter YAML、/adapter_runs 接口)必须等待并迁移到 Tinker 网关形态;单租户 LoRA 与 miles/backends/megatron_utils/lora_utils.py 路径不受影响(#3245/#3083 针对该路径的修复仍然成立)。对系统:仓库净减约 5000 行、72 个文件,multi-LoRA 的复杂度从“驱动 + 控制面 + 数据面 + per-slot 调度”收敛为“槽位原语 + Tinker 网关”两层,长期维护面明显收窄,但短期内 multi-LoRA 能力在 main 上处于真空。对团队:作为 7 PR 栈的第 0 个,它决定了后续评审者对“保留 vs 删除”边界的共同理解,miles/backends/megatron_utils/lora/、miles/utils/multi_lora.py 的保留清单此后就是 v2 的契约面。
- 风险标记:功能删除型 breaking 变更, v1 入口与 HTTP API 缺失替代, 删除测试造成回归盲区, 数据面不变量(staleness/注册隔离)待 v2 等价重建, per-adapter 调度与 YAML 契约不可恢复, AdapterRef 等 import 残留风险, MoE 专家路径恢复完整性待核
关联脉络
- PR #2840 [multi-lora v2 1/7] LoRA model extension and slot actor: PR body 明确 'The Tinker command executor builds on these primitives in #2840',它直接复用本 PR 保留在 miles/backends/megatron_utils/lora/ 下的槽位原语,并新增每租户独立优化器,取代被删的 zero_optimizer_state_for_adapter 清零方案。
- PR #2842 [multi-lora v2 3/7] Per-slot checkpoints with optimizer state: v2 的 per-slot checkpoint 方案取代本 PR 删除的 v1 checkpoint 流程(multi_lora_utils.py 的 save_multi_lora_checkpoints、_register_adapter 与 find_latest_checkpoint)。
- PR #2843 [multi-lora v2 4/7] Publish immutable adapter snapshots to disk: 本 PR 删掉了 v1 的 adapter checkpoint 落盘与 HF 导出路径,v2 改由不可变快照发布机制承担,属于同一功能线的直接替代。
- PR #2844 [multi-lora v2 5/7] Tinker gateway protocol face: 本 PR 删除的 AdapterRegistry/MultiLoRABackend/MultiLoRAHTTPServer 控制面(租户生命周期、注册校验、/adapter_runs 接口)在 v2 中由 Tinker 网关的 core/service.py、core/scheduler.py 承接,是删除清单的主要去向。
- PR #2849 [multi-lora v2 7/7] Tinker gateway example and tests: 替代本 PR 删除的 examples/multi_lora/run_multi_lora.py 与 service_smoke.py,v2 的示例启动器与端到端冒烟测试在该 PR 中重建。
- PR #1794 MoE expert adapter support(标题未在本 PR 材料中给出): issue 评论明确引用其 build-time 三处逻辑(targets_expert_leaves、permute-fusion disable、post-finalize MoE geometry 校验),这些内容最初在删除范围内,最终恢复保留。
参与讨论