Prhub

#2839 [multi-lora v2 0/7] Remove the multi-LoRA v1 implementation

原始 PR 作者 yueming-yuan 合并时间 2026-09-16 16:24 文件变更 72 提交数 1 评论 1 代码增减 +229 / -5202

执行摘要

删除 multi-LoRA v1 实现,仅保留槽位原语

按 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 特有逻辑。

值得精读,但重点不是 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 的评审记录非常薄: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 为准而非提交历史。

实现拆解

  1. 删除 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 栈顺序合入。

  2. 拆除控制面(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)承接,槽位本身的簿记不再由控制面持有。

  3. 拆除数据面(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 里等价重建。

  4. 保留并搬迁槽位原语:新增 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 计划内。

  5. 测试、文档与校验配套:同步删除 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 槽位管理 added 8.79
miles/backends/megatron_utils/multi_lora_utils.py 槽位管理 removed 8.89
miles/rollout/multi_lora/async_rollout.py 异步采样 removed 9.14
miles/ray/multi_lora/registry.py 租户注册 removed 9.08
miles/ray/multi_lora/backend.py 控制面 removed 8.9
miles/utils/multi_lora.py 公共工具 modified 8.14
miles/rollout/multi_lora/data_source.py 数据源 removed 8.74
miles/ray/multi_lora/controller.py 控制面 removed 8.65
miles/ray/multi_lora/http_server.py 控制面 removed 8.63
miles/backends/megatron_utils/multi_lora_scheduler.py 调度器 removed 8.64
miles/utils/adapter_config.py 配置契约 removed 8.38
train_multi_lora_async.py 启动入口 removed 7.89
tests/fast/ray/multi_lora/test_controller_backend.py 测试用例 removed 7.63

关键符号

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 core-logic

本 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 deletion

本次最大单文件删除(-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 deletion

被删除的控制面核心(-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

评论区精华

MoE 专家适配器(#1794)的 build-time 逻辑是删还是留 设计

issue 评论中作者修正了初版删除清单:'MoE expert adapter support (#1794's build-time pieces — targets_expert_leaves, the permute-fusion disable, the post-finalize MoE geometry validation) is now kept, not removed: restored in the trailing commit.' 其余专家路径(EP-aware shard 拓扑、expert-axis-safe rank slicing)本就在保留清单里。

结论:最终采用保留方案,在 trailing commit 中恢复;但提交列表只有 1 个 squash commit,需以 head_sha 958e1ca 的文件内容确认恢复完整。 · 已解决

删除范围与 v2 落地时序的兼容性风险 question

PR body 说明保留的槽位原语将被 #2840 的 Tinker 命令执行器使用,但本 PR 自身删掉了 v1 的入口(train_multi_lora_async.py)、HTTP API(/adapter_runs)与 adapter YAML 契约;两者之间 main 上不存在可用的 multi-LoRA 路径。review 区没有就此提出问题(仅 claude[bot] 模板评论与 yushengsu-thu 的空正文 APPROVED)。

结论:以 PR 栈顺序合入作为隐式约定;该风险未被显式评审,属于需要持续跟踪项。 · 待处理

本 PR 不新增测试,验证依赖后续 PR 重建 测试

PR body 明示 'This update adds no test cases. Current validation and limitations are recorded on #2846.',实际验证仅为 pre-commit run --all-files 与组件级 git diff --check;同时 6 个以上 fast 测试文件(控制器、批量采集、HTTP、调度、train 脚本等)随源码一并删除。

结论:已知限制,验证与限制记录在 #2846;租户生命周期、slot 版本单调性、batch 记账漂移告警等不变量暂时失去回归保护。 · unresolved

评审过程本身缺少实质交锋 other

claude[bot] 只留下仓库级 Code Review 模板评论(提示可用 @claude review 触发),review_comments 为空;yushengsu-thu 给出 APPROVED 且无正文,作者本人为该 PR 的合并者。

结论:该 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 专家路径恢复完整性待核

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论