Skip to content

Feat/task25 mixture of lora - #255

Open
pophirasawa wants to merge 43 commits into
redai-studio:mainfrom
pophirasawa:feat/task25-mixture-lora
Open

Feat/task25 mixture of lora#255
pophirasawa wants to merge 43 commits into
redai-studio:mainfrom
pophirasawa:feat/task25-mixture-lora

Conversation

@pophirasawa

@pophirasawa pophirasawa commented Aug 10, 2026

Copy link
Copy Markdown

feat(lora): 支持 token 级 Mixture-of-LoRA 训练

Note

完整实验数据、统计脚本和图片保存在公开分支 task25-experiment-artifacts。收尾实验总览见 followup/README.md,三组性能统计见 three-way-window-2-11.json,actor 阶段显存统计见 actor-memory-window-2-11.json

Important

Mixture-of-LoRA 核心功能、CPU/CUDA 回归、200-step 主实验、checkpoint 真实续训,以及全参/单 LoRA/Mixture 性能对照均已完成。

Note

200-step 主实验、checkpoint 续训和三组性能对比基于 feat/task25-mixture-lora@a82eb62,当时的上游基线为 main@5b23011。后续通过 b363c18main@050ab04 合入开发分支,并使用 5d86614 补齐 S3 loader 测试 fixture。最新合并版本没有重复运行 200-step 验收,已完成全量 pytest、Task 25 Megatron CUDA 定向测试和三种训练模式的两步端到端回归,结果见下文。

验收状态

  • Checkpoint 短续训:从 iteration-50 独立快照加载完整 distributed checkpoint 和数据状态,完成 step 50–52。
  • 全参数性能基线:相同 Qwen3-4B、DAPO、colocate、batch、response length、TP/SP 和 BF16 配置完成 12 step,统计 step 2–11。
  • 单 LoRA 对照:rank 16 的现有单 LoRA 路径完成 12 step,使用相同统计窗口。
  • 三组 before/after:已汇总峰值显存、step time、response tokens/s 和 actor train tokens/s。
  • CUDA pytest 复跑:原验收版本的 3 个 Task 25 CUDA 用例全部通过;GPU 可见全量测试为 1312 passed, 15 skipped, 2 failed,两个失败均在当时的 upstream 基线复现。
  • 最新 main 合并回归:合入 main@050ab04 后运行全量 pytest,并分别完成全参、单 LoRA、Mixture-LoRA 的 8 卡 BF16 两步训练。
  • 实验材料:200-step 稳定性、expert 路由、checkpoint 恢复、三组性能统计和图片均已整理到公开数据分支。

已经确认的 upstream 基线问题:test_registry_reinforce_plus_plus.pytest_registry_sft.py 连续运行时存在 2 个 enum identity failure,已在未包含本 PR 改动的 upstream/main@5b23011 复现,不属于本 PR 待修内容。

改动内容

本 PR 为 Relax 增加 token 级 Mixture-of-LoRA 训练和 colocate SGLang rollout 支持。

  • 在 Qwen3 attention 的 QKV projection 和 output projection 中加入多个 LoRA expert,由可训练 router 按 token 选择 Top-K expert 并组合输出。
  • Mixture 模式下冻结 base model,只更新 LoRA expert 和 router。
  • 支持 TP、SP、静态 CP、PP、response mask、router balance loss 和逐 site 路由指标。
  • 增加 Qwen3 SGLang external model,通过现有 colocate 生命周期同步 expert 和 router 权重。
  • 使用 Megatron distributed checkpoint 保存和恢复 expert、router、optimizer、scheduler、iteration、RNG 和数据进度。
  • --lora-num-experts 未设置或等于 1 时继续使用现有单 LoRA 路径。
  • 增加 Qwen3-4B DAPO GRPO recipe、中英文文档,以及单元和分布式测试。

关联设计 Issue:#220
参与成员:@pophirasawa@zTonyZhao

改动清单

范围 主要文件 具体改动
CLI 与配置 relax/utils/arguments.pyrelax/utils/megatron_peft_utils.py 增加 expert 数量、Top-K、temperature 和 aux loss coefficient 参数;校验参数组合;区分单 LoRA 与 Mixture 模式;统计 base、expert、router 的参数数量和可训练状态。
公共路由逻辑 relax/utils/mixture_lora.py 定义 MixtureLoraConfig、state/transport schema、route_topk()、路由统计和 balance loss;提供训练端与 rollout 端共用的 RoutedLoRAExecutor 接口。
Megatron routed adapter relax/backends/megatron/mixture_lora.py 实现 MixtureLoRAExpertsMixtureLoRARouterMixtureLoRAAdapterMixtureParallelLinearAdapter;处理 qkv/proj 的 TP/SP 布局、response mask、aux loss 和 checkpoint metadata。
模型注入与训练循环 relax/backends/megatron/model_provider.pymodel.pyloss.py 通过 Bridge PEFT 注入 routed adapter;建立每个 microbatch 的路由上下文;将 aux loss 接入反向传播;在训练 step 结束后聚合逐 site 和全局指标。
Checkpoint relax/backends/megatron/mixture_lora.pymodel.py 将 expert、router 和配置元数据接入 Megatron distributed checkpoint;保留 optimizer、scheduler、iteration、RNG 和 dataset state;支持 DP size 改变后的 optimizer reshard。
SGLang external model relax/models/qwen3_mixture_lora/sglang/model.py 扩展 Qwen3 model,在 qkv/proj 上安装 SGLangMixtureLoRA;实现权重加载、prefill、decode 和 CUDA graph 下的 routed forward。
Colocate 权重同步 relax/backends/megatron/weight_update/mixture_lora_sync.pyupdate_weight_from_tensor.py 收集 PP stage 参数、合并 TP shard、转换 qkv 布局,按稳定 schema 分块发送 expert/router;首次同步保留 base,后续只更新 Mixture 参数。
SGLang 启动接入 relax/backends/sglang/sglang_engine.pyrelax/utils/env.py 在子进程启动前传入 external model package 和 Mixture 配置;保持现有 pause、flush、update、continue 生命周期。
Recipe 与文档 scripts/training/text/run-qwen3-4B-mixture-lora-8xgpu.shdocs/*/guide/mixture-lora.md 增加 Qwen3-4B DAPO colocate BF16 recipe;说明启动条件、参数、指标、checkpoint 恢复和限制。
测试 tests/utils/*mixture_lora*tests/backends/*mixture_lora*tests/models/qwen3_mixture_lora/* 覆盖公共公式、注入、冻结、并行布局、aux loss、指标、checkpoint、SGLang、同步和单 LoRA 回归。

本分支相对 main@050ab04 新增或修改 37 个文件。第 37 个文件是独立提交 5d86614 补充的 S3 loader 测试 fixture,用于给伪造的 PEFT 模块提供新增 Mixture 符号。代码、测试和文档均包含在本 PR 中,提交历史按配置、训练端、分布式、checkpoint、rollout、同步、recipe 和修复逐步拆分,便于分别审查。

官方验收标准对应关系

验收项 本 PR 的实现 当前证据 状态
--lora-num-experts 4 --lora-rank 16 可启用 新增 Mixture 参数解析、组合校验和 Bridge PEFT 注入 参数单测、Qwen3-4B 200-step 启动日志 已完成
base 冻结,只更新 expert 和 router 对 base、expert、router 分类校验 requires_grad,optimizer 只接收可训练参数 启动时可训练参数检查;base 无梯度、expert/router 有梯度的单测 已完成
单 expert 与现有 LoRA 兼容 N=1 继续使用原有 ParallelLinearAdapter、checkpoint 和 rollout 路径 参数名、前向输出、checkpoint、rollout 回归测试及单 LoRA 12-step 对照 已完成
Qwen3-4B、DAPO math、colocate GRPO ≥200 step 提供 8 卡 BF16 recipe,训练和 rollout 共卡运行 8×A800 80GB 完成 200/200 step,1474 组 scalar 无 NaN/Inf 已完成
全参、单 LoRA、Mixture 显存与吞吐对比 统一模型、数据、batch、response length、TP/SP、BF16 和 step 2–11 窗口 三组时间、吞吐、response 长度、actor 阶段显存和端到端峰值已汇总 已完成
输出 expert 平均激活权重且不塌缩 按 routed site 和全局记录激活权重、入选份额、Top-1 比例和熵 最后 50 step 全局权重为 [0.2502, 0.2457, 0.2501, 0.2540],72 个 site 无单 expert 塌缩 已完成
checkpoint 保存/恢复后一致 使用 Megatron distributed checkpoint 保存 expert、router 及全部训练状态,支持 DP reshard 固定 batch、恢复后下一步、DP 2→1 和 iteration-50 真实续训均通过 已完成
查看 36 个变更文件的逐文件说明
文件 修改内容
docs/.vitepress/config.mts 将 Mixture-of-LoRA 中英文指南加入文档导航。
docs/en/guide/mixture-lora.md 说明启动参数、路由指标、checkpoint、rollout 与限制。
docs/zh/guide/mixture-lora.md 增加与英文版对应的中文使用指南。
relax/utils/arguments.py 注册 expert 数量、router Top-K、temperature 和 aux loss coefficient 参数。
relax/backends/megatron/arguments.py 将 Mixture 参数接入 Megatron 参数检查和运行配置。
relax/utils/megatron_peft_utils.py 判断单 LoRA/Mixture 路径,构建配置,识别 expert/router 参数并校验可训练集合。
relax/utils/mixture_lora.py 定义公共配置、state/transport schema、Top-K 路由、统计、balance loss 和 executor 接口。
relax/backends/megatron/mixture_lora.py 实现 expert/router/adapter、dense executor、TP/SP collective、aux loss 梯度接入、路由上下文和 sharded checkpoint。
relax/backends/megatron/model_provider.py 通过 Bridge PEFT 注入 adapter,安装 checkpoint/recompute 上下文,执行启动参数检查。
relax/backends/megatron/model.py 管理 microbatch 路由上下文,聚合逐 site/全局指标,并调用 Mixture 权重同步。
relax/backends/megatron/loss.py 向路由上下文提供主 loss 的 token/microbatch 缩放信息。
relax/utils/megatron_bridge_utils.py 让 Bridge 模型转换和 PEFT 流程识别 Mixture 模式。
relax/backends/megatron/weight_update/common.py 将 Mixture 参数元数据接入权重更新协议。
relax/backends/megatron/weight_update/hf_weight_iterator_bridge.py 在 Bridge 转换路径中区分 base 与 Mixture 参数。
relax/backends/megatron/weight_update/hf_weight_iterator_direct.py 在 direct 转换路径中区分 base 与 Mixture 参数。
relax/backends/megatron/weight_update/mixture_lora_sync.py 实现 PP 参数收集、TP shard 合并、qkv 布局转换和 expert/router 分块发送。
relax/backends/megatron/weight_update/update_weight_from_tensor.py 将 Mixture 同步器接入现有 pause/flush/update/continue 权重更新流程。
relax/utils/env.py 为 SGLang 子进程传递 external model package 和序列化的 Mixture 配置。
relax/backends/sglang/sglang_engine.py 在 engine 启动前配置 Qwen3 external model,并在更新路径选择 Mixture 协议。
relax/models/qwen3_mixture_lora/__init__.py 声明 Qwen3 Mixture external model package。
relax/models/qwen3_mixture_lora/sglang/__init__.py 导出 SGLang external model 实现。
relax/models/qwen3_mixture_lora/sglang/model.py 在 Qwen3 qkv/proj 上安装 adapter,实现参数校验与 prefill/decode/CUDA graph 前向。
scripts/training/text/run-qwen3-4B-mixture-lora-8xgpu.sh 增加 Qwen3-4B、DAPO math、GRPO、colocate、BF16 验收 recipe。
tests/utils/test_arguments_mixture_lora.py 测试 CLI 参数行为、启用条件和无效组合。
tests/utils/test_megatron_peft_utils.py 测试单 LoRA fallback、Mixture 参数分类和冻结校验。
tests/utils/test_mixture_lora_routing.py 使用独立参考计算测试 Top-K、dense executor、mask、统计和 balance loss。
tests/backends/megatron/test_mixture_lora.py 测试 adapter 前向/反向、冻结、aux loss、recompute、Bridge 注入和 checkpoint。
tests/backends/megatron/test_mixture_lora_arguments.py 测试 Mixture 与 Megatron 并行参数的组合校验。
tests/backends/megatron/test_mixture_lora_distributed.py 对比 DP=1 参考与 DP=2、TP=2、SP、CP=2、PP=2 的输出、梯度、aux loss 和指标。
tests/backends/megatron/test_mixture_lora_checkpoint_distributed.py 测试 PP stage 参数归属和 DP 2→1 parameter/optimizer reshard。
tests/backends/megatron/test_model_provider_vpp.py 回归测试 VPP 下的 PEFT 注入与 site id 构造。
tests/backends/megatron/test_sft_chunked_ce.py 回归测试 loss/recompute 路径与 microbatch objective scale 集成。
tests/backends/megatron/weight_update/test_mixture_lora_weight_sync.py 测试 TP/PP 参数合并、首次/后续同步、错误校验和 rollout 版本提交。
tests/backends/sglang/test_mixture_lora.py 测试 SGLang 配置传入、external model 启动和 Mixture 同步路径。
tests/backends/sglang/test_router_registration.py 更新伪模块 fixture,覆盖新增 Mixture import 后的原有 router 注册回归。
tests/models/qwen3_mixture_lora/test_sglang_model.py 测试 external model 安装、schema/shape/dtype 校验、在线更新与 routed forward。

背景

单个 LoRA adapter 对复杂 RL 数据的容量有限。Task 25 希望在 base model 保持冻结的前提下,让多个 LoRA expert 共同参与训练,并由 router 根据 token 动态组合 expert。同时还需要保留单 LoRA 兼容性、支持 colocate GRPO、输出可判断路由是否塌缩的指标,并保证 checkpoint 可以正确保存和恢复。

使用的主要技术

技术或现有组件 在本 PR 中的用途
PyTorch nn.Module 与 autograd 定义 expert/router 参数、执行 routed LoRA 前向;使用自定义 autograd function 将 aux loss 梯度接入本地输出,并为 TP collective 定义正确的反向行为。
Megatron Core model parallel 复用 TP、SP、PP、CP process group 和 tensor layout;qkv 使用 column-parallel 契约,proj 使用 row-parallel 契约。
Megatron Bridge PEFT 沿用 Relax 当前的 PEFT 注入入口,在目标 linear_qkvlinear_proj 外包一层 MixtureParallelLinearAdapter,保持 base 模型构建和参数名称稳定。
Megatron distributed checkpoint 保存 sharded expert/router 及其配置元数据,并复用 distributed optimizer 的 fully reshardable 状态恢复。
torch.distributed collective 对 TP shard 执行 gather/reduce,对 replicated router 梯度执行 all-reduce;跨 PP stage 收集待同步参数信息。所有 collective 显式使用对应 process group。
SGLang external model 在不修改 SGLang 上游 Qwen3 实现的情况下接入 routed qkv/proj,支持 prefill、decode、CUDA graph 和在线权重更新。
Relax colocate 与权重更新流程 复用 actor/rollout 共卡部署和 pause -> flush -> update -> continue 流程,不新增 manager、service 或 barrier。
Ray 继续使用 Relax 现有 actor 与 rollout worker 生命周期和资源分配;Mixture 没有引入额外 Ray 服务。
TensorBoard 与 Metrics Service 记录训练稳定性、吞吐、balance loss,以及逐 site/全局 expert 路由指标。

数据流

flowchart LR
    X[Token hidden state] --> Base[冻结的 base linear]
    X --> Router[可训练线性 router]
    Router --> Prob[FP32 softmax / temperature]
    Prob --> TopK[Top-K 选择与重新归一化]
    X --> Experts[LoRA A/B experts]
    TopK --> Combine[按选中权重组合 expert 输出]
    Experts --> Combine
    Base --> Add[Base 输出 + routed LoRA delta]
    Combine --> Add
    TopK --> Stats[response token 路由统计]
    Stats --> Aux[balance loss]
    Stats --> Metrics[逐 site 与全局指标]
Loading

一次 colocate optimizer step 的权重流如下:

flowchart LR
    Train[Megatron actor] --> Update[更新 expert + router]
    Update --> Gather[合并 TP shard / 收集 PP stage]
    Gather --> Transport[Mixture transport schema]
    Transport --> Pause[暂停 SGLang generation]
    Pause --> Load[SGLang 加载 expert + router]
    Load --> Resume[恢复 rollout]
    Update --> Ckpt[Megatron distributed checkpoint]
Loading

端到端调用链

  1. relax/utils/arguments.py 解析 LoRA 与 router 参数,build_mixture_lora_config() 生成 MixtureLoraConfig,无效的 expert、Top-K、rank 和并行组合会在模型创建前报错。
  2. model_provider.py 调用 build_mixture_lora_peft(),通过 Megatron Bridge PEFT 在每个目标 linear_qkvlinear_proj 上注入 MixtureParallelLinearAdapter,然后检查 base 冻结状态和 expert/router 可训练参数数量。
  3. 训练 forward 开始前,model.py 为当前 microbatch 创建 MixtureLoRARoutingContext,其中包含 response mask、objective scale 和本次 forward 的路由记录容器。
  4. adapter forward 中,MixtureLoRARouter 生成 logits,route_topk() 完成 FP32 softmax、Top-K 选择和权重重新归一化,MegatronDenseRoutedLoRAExecutor 计算并组合各 expert 的 LoRA delta。
  5. compute_routing_statistics() 按 response mask 计算每个 site 的入选份额、概率均值、激活权重和熵,mean_routing_balance_loss() 得到当前 microbatch 的 aux loss。
  6. _AttachAuxLoss 把 aux loss 的梯度接到当前 PP stage 的 adapter 输出。get_microbatch_objective_scale() 复用主 loss 的 token/microbatch 缩放信息,保证 dynamic batch、dummy batch 和 recompute 下不重复或遗漏 aux 梯度。
  7. optimizer step 结束后,pack_mixture_lora_routing_records() 将本地记录整理为固定形状 tensor,_reduce_mixture_lora_routing_metrics() 按 TP/DP/CP/PP 归约,再将逐 site 和全局指标交给 Metrics Service。
  8. 在 colocate 更新中,MixtureLoraSync 收集各 PP stage 的 expert/router,merge_mixture_lora_tp_shards() 还原全局逻辑 tensor,然后按 TransportTensorSpec 分块发送给 rollout。
  9. sglang_engine.py 在 SGLang 子进程启动前注入 external model package 和 Mixture 配置。SGLangMixtureLoRA 安装到 Qwen3 的 qkv/proj,load_sglang_mixture_lora_weights() 验证 schema、site、shape 和 dtype 后更新参数。

实现方式

启用参数

参数 作用 验收 recipe 使用值
--lora-num-experts 每个 routed site 的 expert 数量;大于 1 时启用 Mixture 4
--lora-rank 每个 expert 的 LoRA rank 16
--lora-alpha LoRA 缩放系数中的 alpha 32
--lora-router-top-k 每个 token 参与组合的 expert 数量 2
--lora-router-temperature router softmax temperature 1.0
--lora-router-aux-loss-coef balance loss 加到训练目标时的系数 0.01
--lora-target-modules 安装 routed adapter 的 projection linear_qkv linear_proj

这些值是当前 Qwen3-4B 验收 recipe 的配置,不是全部参数的通用默认值。Mixture 模式要求显式提供 router 参数,Top-K 必须满足 1 <= K <= N;column-parallel 路径还要求 rank 可以按 TP size 正确切分。

运行 200-step 训练

在具有 8 张可用 GPU 的训练节点上,进入 Relax 仓库根目录,替换模型、数据和输出路径后执行:

cd /path/to/Relax

MODEL_PATH=/path/to/Qwen3-4B \
PROMPT_DATA=/path/to/dapo-math-17k.jsonl \
OUTPUT_DIR=/path/to/mixture-lora-output \
NUM_ROLLOUT=200 \
SAVE_INTERVAL=50 \
bash scripts/training/text/run-qwen3-4B-mixture-lora-8xgpu.sh

该 recipe 直接设置 4 个 expert、rank 16、Top-K 2、BF16、TP=2 和 SP。actor 与 rollout 都申请 8 张 GPU,并通过 --colocate 共用同一组设备,因此实际需要 8 张 GPU。本次实验使用 8×A800 80GB,完成 200/200 step。

完整 recipe 位于 scripts/training/text/run-qwen3-4B-mixture-lora-8xgpu.sh

#!/bin/bash

set -ex

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
if [[ -z "${RELAX_ENTRYPOINT_MODE:-}" ]]; then
    source "${SCRIPT_DIR}/../../entrypoint/local.sh"
fi
source "${MODEL_CONFIG_DIR}/qwen3-4B.sh"

now="$(date '+%Y-%m-%d-%H-%M-%S')"
PROJECT_NAME="${PROJECT_NAME:-Relax/dev/qwen3-4b-mixture-lora}"
EXP_DIR="${EXP_DIR:-${SCRIPT_DIR}/../../../../exps}"
MODEL_DIR="${MODEL_DIR:-${EXP_DIR}}"
DATA_DIR="${DATA_DIR:-${EXP_DIR}}"
MODEL_PATH="${MODEL_PATH:-${MODEL_DIR}/Qwen3-4B}"
PROMPT_DATA="${PROMPT_DATA:-${DATA_DIR}/dapo-math-17k/dapo-math-17k.jsonl}"
OUTPUT_DIR="${OUTPUT_DIR:-${EXP_DIR}/Qwen3-4B_mixture_lora_8xgpu}"
NUM_ROLLOUT="${NUM_ROLLOUT:-200}"

CKPT_ARGS=(
    --hf-checkpoint "${MODEL_PATH}"
    --ref-load "${MODEL_PATH}"
    --megatron-to-hf-mode bridge
    --warm-hf-checkpoint-page-cache
    --load "${OUTPUT_DIR}"
    --save "${OUTPUT_DIR}"
    --save-interval "${SAVE_INTERVAL:-50}"
    --dist-ckpt-optim-fully-reshardable
)

LORA_ARGS=(
    --lora-rank "${LORA_RANK:-16}"
    --lora-alpha "${LORA_ALPHA:-32}"
    --lora-target-modules linear_qkv linear_proj
    --lora-dropout 0.0
    --lora-num-experts "${LORA_NUM_EXPERTS:-4}"
    --lora-router-top-k "${LORA_ROUTER_TOP_K:-2}"
    --lora-router-temperature "${LORA_ROUTER_TEMPERATURE:-1.0}"
    --lora-router-aux-loss-coef "${LORA_ROUTER_AUX_LOSS_COEF:-0.01}"
)

ROLLOUT_ARGS=(
    --prompt-data "${PROMPT_DATA}"
    --input-key prompt
    --label-key label
    --apply-chat-template
    --rollout-shuffle
    --rm-type dapo
    --reward-key score
    --num-rollout "${NUM_ROLLOUT}"
    --rollout-batch-size "${ROLLOUT_BATCH_SIZE:-16}"
    --n-samples-per-prompt "${N_SAMPLES_PER_PROMPT:-8}"
    --rollout-max-response-len "${ROLLOUT_MAX_RESPONSE_LEN:-8192}"
    --rollout-temperature 1.0
    --global-batch-size "${GLOBAL_BATCH_SIZE:-128}"
    --balance-data
    --use-fault-tolerance
)

GRPO_ARGS=(
    --advantage-estimator grpo
    --use-kl-loss
    --kl-loss-coef 0.0
    --kl-loss-type low_var_kl
    --entropy-coef 0.0
    --eps-clip 0.2
    --eps-clip-high 0.28
    --use-tis
)

OPTIMIZER_ARGS=(
    --optimizer adam
    --lr "${LR:-1e-5}"
    --lr-decay-style constant
    --weight-decay 0.1
    --adam-beta1 0.9
    --adam-beta2 0.98
    --use-precision-aware-optimizer
    --no-store-param-remainders
)

PARALLEL_ARGS=(
    --tensor-model-parallel-size 2
    --sequence-parallel
    --pipeline-model-parallel-size 1
    --context-parallel-size 1
    --expert-model-parallel-size 1
    --expert-tensor-parallel-size 1
    --recompute-granularity full
    --recompute-method uniform
    --recompute-num-layers 1
    --calculate-per-token-loss
    --use-dynamic-batch-size
    --max-tokens-per-gpu "${MAX_TOKENS_PER_GPU:-9216}"
    --log-probs-max-tokens-per-gpu "${LOG_PROBS_MAX_TOKENS_PER_GPU:-30720}"
)

SGLANG_ARGS=(
    --rollout-num-gpus-per-engine 1
    --sglang-mem-fraction-static "${SGLANG_MEM_FRACTION_STATIC:-0.7}"
)

LOGGING_ARGS=(
    --use-metrics-service
    --tb-project-name "${PROJECT_NAME}"
    --tb-experiment-name "qwen3-4b-mixture-lora-${now}"
)

MISC_ARGS=(
    --attention-dropout 0.0
    --hidden-dropout 0.0
    --accumulate-allreduce-grads-in-fp32
    --attention-softmax-in-fp32
    --attention-backend flash
    --skip-eval-before-train
)

mkdir -p "${OUTPUT_DIR}" "${OUTPUT_DIR}/logs"
if [[ -z "${RUNTIME_ENV_JSON:-}" ]]; then
    RUNTIME_ENV_JSON='{}'
fi

ray job submit ${RAY_NO_WAIT:+--no-wait} --address="${RAY_ADDRESS:-http://127.0.0.1:8265}" \
    ${WORKING_DIR:+--working-dir "${WORKING_DIR}"} \
    --runtime-env-json="${RUNTIME_ENV_JSON}" \
    -- python3 -m relax.entrypoints.train \
    --resource '{"actor": [1, 8], "rollout": [1, 8]}' \
    --max-staleness 0 \
    --num-data-storage-units 1 \
    --colocate \
    --bf16 \
    --use-health-check \
    "${MODEL_ARGS[@]}" \
    "${CKPT_ARGS[@]}" \
    "${ROLLOUT_ARGS[@]}" \
    "${OPTIMIZER_ARGS[@]}" \
    "${GRPO_ARGS[@]}" \
    "${LOGGING_ARGS[@]}" \
    "${PARALLEL_ARGS[@]}" \
    "${SGLANG_ARGS[@]}" \
    "${LORA_ARGS[@]}" \
    "${MISC_ARGS[@]}" \
    "$@" 2>&1 | tee "${OUTPUT_DIR}/logs/qwen3-4b-mixture-lora-${now}.log"

参数与兼容性

--lora-num-experts 大于 1 时启用 Mixture 模式。--lora-rank--lora-alpha--lora-target-modules--lora-dropout 保持原有含义,并新增 expert 数量、router Top-K、router temperature 和 aux loss coefficient。参数解析阶段会拒绝 Top-K 大于 expert 数量等无效配置。

expert 数量未设置或等于 1 时,Relax 继续使用现有 ParallelLinearAdapter 注入、checkpoint 导出和 SGLang LoRA 路径,不启用 Mixture external model 和 Mixture 权重传输格式。

参数、checkpoint 与传输布局

每个 routed site 都保存三组可训练参数:

状态名称 全局逻辑 shape 用途
mixture_lora.experts.lora_A [num_experts, rank, input_size] 将 token hidden state 投影到每个 expert 的 rank 空间
mixture_lora.experts.lora_B [num_experts, output_size, rank] 将每个 expert 的 rank 表示投影回 linear 输出空间
mixture_lora.router.weight [num_experts, input_size] 为每个 token 生成 expert logits

linear_qkv 遵循 column-parallel 布局:lora_A 的 rank 维按 TP 切分,lora_B 的 output 维按 TP 切分,router 在 TP rank 上复制并 all-reduce 梯度。linear_proj 遵循 row-parallel 布局:lora_A 和 router 的 input 维按 TP 切分,lora_B 的 output 维按 TP 切分。executor 使用与 Megatron linear 层一致的 gather/reduce 约定恢复完整 routed delta。

checkpoint 中的参数 key 与上表名称一致,每个 adapter 还保存 schema_versionsite_id、expert 数量、rank、Top-K、temperature、aux coefficient、alpha、target modules、输入/输出尺寸和 dtype。权重同步通过 MixtureLoraStateSpecTransportTensorSpec 携带相同的 site、参数类型、全局 shape 和 dtype,SGLang 在写入前逐项校验。

训练端

每个 routed projection 包含多组 LoRA A/B expert 参数和一个可训练线性 router。router 为每个 token 计算 expert logits,选取 Top-K expert,对入选权重重新归一化,再组合对应 LoRA 输出。

首版执行器使用 dense 计算,并通过独立 executor 接口与路由语义、参数 schema、checkpoint key 和权重传输名称隔离。以后替换为 grouped sparse kernel 时,可以继续使用相同的模型参数与 checkpoint。

每个 token 的路由过程为:

  1. MixtureLoRARouter 使用输入 hidden state 计算 FP32 logits。
  2. route_topk() 在 FP32 中执行 temperature softmax 和 Top-K。
  3. 对入选概率重新归一化,使每个 token 的 Top-K 权重和为 1。
  4. MegatronDenseRoutedLoRAExecutor 计算各 expert 的 B(A(x)),再按路由权重求和。
  5. routed delta 乘以 alpha / rank,与冻结 base linear 的输出相加。

首版 dense executor 会计算全部 expert,再将未入选 expert 的权重置零。这保证公式、梯度和分布式布局先稳定下来,训练端 grouped sparse kernel 属于后续性能优化,不影响当前 checkpoint 和 rollout 参数格式。

Mixture 模式下 base model 参数保持冻结。expert 和 router 使用独立参数分类,并作为模型中仅有的可训练参数加入 optimizer。routed module 通过现有 Megatron Bridge PEFT 流程注入,避免改变 base 参数名称和模型构建方式。

router balance loss 使用 expert 入选频率和平均路由概率计算,只统计有效 response token。每个 router 的 aux loss 在其所属 PP stage 本地接入反向传播,复制参数的梯度继续由现有分布式同步处理。

TP/SP 下 routed projection 遵循周围 Megatron 线性层的数据布局。静态 CP 会在 CP group 内汇总路由计数,同时保留各 rank 本地可导的概率贡献。PP 下 routed module、expert 和 router 留在目标 transformer layer 所在 stage,不跨 stage 传输 router tensor。

路由指标

每个 optimizer step 都会记录 balance loss、逐 site 指标和全局指标,包括:

  • Top-K 后各 expert 平均激活权重;
  • 各 expert 入选份额;
  • 各 expert 成为 Top-1 的比例;
  • Top-K 前后归一化熵;
  • 有效 response token 数量。

是否发生 expert 塌缩主要根据跨 token 聚合后的激活权重和入选份额判断。逐 token 熵用于观察 router 的选择置信度,不单独作为塌缩门槛。

balance loss 按 routed site 独立计算后取平均。对 expert e

  • F_e = selection_count_e / (valid_response_tokens * K),表示 Top-K 入选份额,作为离散统计量不参与反向传播;
  • P_e 是有效 response token 上 Top-K 前 router 概率的均值,保留梯度;
  • site loss 为 N * sum(F_e * P_e)

F_e 的分母包含 K,因此均匀路由时的 loss 基线不会随 Top-K 改变。

SGLang rollout 与权重同步

SGLang external model 与训练端共用路由函数和参数 schema。首次同步发送 base、全部 expert 和 router;后续 optimizer step 只发送 expert 和 router,并复用现有 pause -> update -> resume 流程。

Mixture 配置会在 SGLang 子进程启动前注入。权重加载会检查 schema version、site、参数类型、全局 shape、dtype 和 Mixture 配置。prefill、decode、CUDA graph capture 和在线权重更新均使用与训练端一致的 token Top-K 语义。

Checkpoint

expert 和 router 进入现有 Megatron distributed checkpoint。恢复时会校验 Mixture 配置和 site schema;TP、PP 不变时,fully reshardable distributed optimizer 支持改变 DP size 后继续恢复。

checkpoint 覆盖模型参数、optimizer、scheduler、iteration、RNG 和数据进度。Mixture 模式不复用标准单 adapter PEFT 导出,因为每个 routed site 包含多个 expert 和 router;现有单 LoRA 导出行为保持不变。

测试

已经完成:

  • 232 个定向 pytest,覆盖路由、参数校验、模型注入、balance loss、指标、checkpoint、TP/SP/CP/PP、SGLang 加载和权重同步。
  • TP=2 且 SP 开启/关闭、静态 CP=2、PP=2 和 DP resharding 测试。
  • Qwen3-4B colocate 冒烟测试,覆盖 SGLang CUDA graph、在线权重更新,以及 DP 从 2 缩到 1 后恢复 checkpoint。
  • 在 8×A800 80GB 上使用 Qwen3-4B、DAPO math、colocate、TP=2、SP 和 BF16 连续完成 Mixture-of-LoRA GRPO 200/200 step。
  • TensorBoard 的 1474 组 scalar 均为有限值。raw reward 均值从前 50 step 的 -0.4766 提升到最后 50 step 的 0.3475。
  • 最后 50 step 的全局 expert 激活权重为 [0.2502, 0.2457, 0.2501, 0.2540]。72 个 routed site 均未塌缩到单一 expert,单个 site 的最大 expert 平均权重为 0.4110。
  • Mixture 实验采样到的单卡峰值显存为 64,084 MiB,最后 50 step 平均耗时为 234.19 秒/step。
  • 8×A800 可见 GPU 环境中的 3 个 Task 25 CUDA 用例全部通过;完整测试集为 1312 passed, 15 skipped, 2 failed,两个失败均在 upstream 5b23011 复现。
  • 从 iteration 49 的独立快照恢复完整 distributed checkpoint 和数据状态后,成功完成 step 50–52。
  • 87dfe2f 的干净 worktree 上完成全仓库 pre-commit:除 Gitleaks 外的全部 hook 通过;Gitleaks 因离线环境无法初始化远端 hook,改为单独运行仓库自带的 tracked-files 包装脚本,扫描约 9.26 MB,结果为 no leaks found。两次检查后 worktree 保持干净。
  • 在隐藏已占用 GPU 的离线环境中运行完整 CPU 可运行测试集,结果为 1307 passed, 17 skipped, 5 deselected,耗时 6 分 47 秒。
  • 合入 main@050ab04 后重新运行完整 pytest tests/。测试主体完成 1568 passed, 12 skipped;一个分布式测试首次运行遇到随机端口占用,相关文件独立重跑 3 passed,因此合并版本的全量测试通过。
  • 合并版本的 4 个 Task 25 Megatron 定向文件在 CUDA 可见环境中完成 61 passed,无 skip、无失败,覆盖真实 Bridge PEFT、CUDA forward/backward profiler、分布式 checkpoint、DP/TP/SP/CP/PP 和权重同步。
  • S3 loader fixture 修复后,tests/test_s3_model_loader.py 完成 51 passed
  • 合并版本分别完成全参、单 LoRA、Mixture-LoRA 的 8 卡 BF16 两步 Qwen3-4B DAPO colocate 训练,三组均完成 actor 训练、rollout、权重同步并正常退出。
测试范围 覆盖内容 结果
路由计算 Top-K、权重归一化、mask、dense executor 前向和反向 通过
参数行为 base 冻结、expert/router 可训练、单 LoRA fallback 通过
分布式训练 DP=2、TP=2、SP 开关、静态 CP=2、PP=2 通过
Balance loss 与指标 response mask、DP/TP/CP 聚合、逐 site 和全局统计 通过
Checkpoint 固定 batch 恢复、下一步恢复、PP 参数归属、DP 2→1 reshard、iteration-50 真实续训 通过
SGLang rollout 配置传输、模型加载、prefill、decode、CUDA graph、在线更新 通过
端到端训练 Qwen3-4B、DAPO math、colocate、BF16、200 step 通过
GPU 可见全量回归 完整 pytest tests/,并与 upstream 对照失败项 1312 passed、15 skipped;2 个 upstream 失败
最新 main 合并回归 全量 pytest、Task 25 Megatron CUDA 定向测试、S3 fixture、三种两步短训 全部通过

最新 main 合并验证

b363c18main@050ab04 合入开发分支,三个冲突分别位于模型导出、Tensor 权重同步和 SGLang 启动。合并时同时保留最新 main 的 S3 模型来源、HF/FP8 导出、post hook、不等长 bucket padding 和 CPU 权重备份逻辑,以及 Task 25 的 Mixture 导出分支、分块 weight version 和 external model 配置。

5d86614 单独修复 tests/test_s3_model_loader.py 的测试 fixture。该 fixture 使用精简版 megatron_peft_utils 导入 sglang_engine.py,因此需要补充 build_mixture_lora_configis_mixture_lora_enabled 两个 stub。两个 stub 固定返回未启用 Mixture,只影响测试替身,不改变 S3、SGLang 或 Mixture 的运行时行为。

合并版本的验证结果如下:

验证范围 结果 说明
完整 pytest tests/ 通过 主测试完成 1568 passed, 12 skipped;一个用例首次遇到随机端口占用,所属文件重跑 3 passed
Task 25 Megatron CUDA 定向测试 61 passed 无 skip;覆盖真实 Bridge、CUDA profiler、checkpoint、DP/TP/SP/CP/PP 和权重同步
S3 loader fixture 回归 51 passed 确认普通 S3 路径未受 Mixture 顶层导入影响
全参两步训练 通过 8 卡 BF16,Qwen3-4B DAPO colocate
单 LoRA 两步训练 通过 Bridge PEFT 注入、rollout 和在线权重同步成功
Mixture-LoRA 两步训练 通过 4 experts、rank 16、Top-K 2;路由指标有限且未塌缩

两步训练用于确认最新 main 合并后不同训练模式均可启动并完成完整 step,不替代下方基于 a82eb62 的 200-step 稳定性和 step 2–11 性能实验。

200-step 实验数据

项目 结果
训练任务状态 succeeded
功能代码 实验时为 6d1d751;实际使用的 BF16 recipe 与文档随后提交为 317cc44
环境 8×A800 80GB,Qwen3-4B,DAPO math,GRPO,colocate,BF16
并行配置 TP=2,SP 开启,PP=1,CP=1
Batch rollout batch 16,8 samples/prompt,global batch 128,response length 上限 8192
连续训练时间 14:02:04,完成 step 0–199
Checkpoint 保存 iteration 49、99、149、199,另保留 iteration-50 独立快照
数值稳定性 1474 个 TensorBoard scalar tag 全部为有限值;grad norm 范围 0.00380–0.01751
指标 全 200 step 前 50 step 最后 50 step
raw reward 均值 0.0503 -0.4766 0.3475
train loss 均值 0.03236 0.01556 0.04369
grad norm 均值 0.01000 0.00957 0.01019
router aux loss 均值 0.01047 0.01081 0.01022
全局 Top-K 前归一化熵 0.8986 0.8895 0.9053
全局 Top-K 后归一化熵 0.9322 0.9251 0.9371
性能指标 全 200 step 最后 50 step
step time 均值 253.67s 234.19s
response tokens/s 2871.57 2717.36
actor train tokens/s 9926.65 9934.79
actor train time 75.60s 66.32s
update weights time 4.32s 4.34s

Mixture 200-step 作业的全程单卡峰值显存为 64,084 MiB(8 卡范围 63,554–64,084 MiB),全程 GPU 平均利用率为 83.92%。三组性能对比使用下面单独定义的统一窗口。

全参、单 LoRA 与 Mixture 性能对比

三组使用相同的模型、数据、batch、response length、TP/SP、BF16 和
sglang-mem-fraction-static=0.7。全参和单 LoRA 各运行 12 step,Mixture 使用
200-step 主实验的相同下标,统一统计 step 2–11。三组平均 response 长度相差不到
0.3%,窗口内没有 checkpoint 保存。

指标 全参 单 LoRA Mixture-LoRA
step time (s) 218.98 219.11 281.68
rollout time (s) 111.72 113.79 155.61
actor train time (s) 78.61 76.92 89.91
response 吞吐 (tok/s) 3,964.85 3,952.03 3,075.76
actor train 吞吐 (tok/s) 11,293.67 11,511.07 9,851.78
actor 阶段系统显存均值 (MiB/GPU) 38,259 26,669 29,158
actor 阶段系统显存 P95 (MiB/GPU) 46,391 30,100 32,190
actor 阶段系统显存峰值 (MiB/GPU) 47,550 31,258 33,678
step 2–11 系统峰值显存 (MiB) 61,632 61,656 63,494
平均 response 长度 (token) 6,787.07 6,767.17 6,773.76

单 LoRA 与全参在本次十步窗口中的 step time 相差 0.06%,未观察到明显的系统吞吐差异。
当前 dense Mixture 路径比单 LoRA 慢 28.56%,其中 rollout 增加 41.82 秒、actor train
增加 12.99 秒;rollout 占额外 step time 的 66.8%。Mixture 的 Top-K 选择依赖 token,
无法像固定单 LoRA 一样静态合并进 base,但当前训练端和 SGLang rollout 端还会计算全部
四个 expert,因此该开销包含可由 grouped sparse 或融合 kernel 优化的部分。

Actor 阶段根据 TensorBoard 的 actor_train_timeupdate_weights_time 和指标写入时间
重建区间,再过滤 5 秒间隔的 NVML 样本。单 LoRA 和 Mixture-LoRA 的 actor 阶段系统
显存均值比全参分别低 30.3% 和 23.8%。三组都设置了
--sglang-mem-fraction-static 0.7,SGLang rollout 会预留相近大小的静态显存池,生成阶段
的 KV cache 和 batch 又会把显存推到该容量附近,因此端到端峰值都接近 60–62 GiB。
这个峰值主要反映统一的 rollout 显存配额,用于判断完整 workload 是否会 OOM;actor
阶段统计才用于比较不同训练方式的显存差异。完整方法、边界和逐项数据见
followup/03_吞吐实验.md
followup/04_三组对比结论.md

Checkpoint 真实续训

在 8×A800 80GB 环境中,从 200-step 主实验保存的 iteration 49 独立快照恢复训练,并继续完成 step 50–52。恢复过程加载了模型参数、fully reshardable optimizer 状态、scheduler、iteration、RNG 和数据状态,没有设置跳过 optimizer/RNG 或重置 optimizer 的参数。

验收点 结果
训练进度 从 iteration 49 恢复,继续运行 step 50、51、52
数据进度 恢复 sample_offset=816sample_index=6528
Optimizer 与 scheduler 完整 checkpoint 加载成功;step 50–52 学习率与主实验一致
训练稳定性 loss、grad norm 和 aux loss 均为有限值,aux loss 与主实验对应 step 的差异小于 0.25%
路由连续性 四个 expert 平均激活权重的最大差异约为 0.003,未出现单 expert 塌缩

恢复任务在完成 step 52 后由 watcher 主动停止,因此 Ray 状态为 STOPPED;这是预定的短续训结束方式,不是训练错误。

测试命令

Mixture 核心功能、并行布局、checkpoint、SGLang 和单 LoRA 回归可以使用以下命令集中运行:

export PYTHONPATH="$PWD:${MEGATRON_PATH}:${SGLANG_PYTHON_PATH}:${PYTHONPATH:-}"

pytest -q \
  tests/utils/test_arguments_mixture_lora.py \
  tests/utils/test_megatron_peft_utils.py \
  tests/utils/test_mixture_lora_routing.py \
  tests/backends/megatron/test_mixture_lora.py \
  tests/backends/megatron/test_mixture_lora_arguments.py \
  tests/backends/megatron/test_mixture_lora_checkpoint_distributed.py \
  tests/backends/megatron/test_mixture_lora_distributed.py \
  tests/backends/megatron/test_model_provider_vpp.py \
  tests/backends/megatron/test_sft_chunked_ce.py \
  tests/backends/megatron/weight_update/test_mixture_lora_weight_sync.py \
  tests/backends/sglang/test_mixture_lora.py \
  tests/backends/sglang/test_router_registration.py \
  tests/models/qwen3_mixture_lora

离线容器中的全仓库静态检查使用:

SKIP=gitleaks pre-commit run --all-files --show-diff-on-failure
python .pre-commit-hooks/gitleaks_tracked.py

Gitleaks 使用服务器已安装的 v8.24.2,第二条命令扫描全部 tracked files。完整 CPU 可运行回归由 CUDA_VISIBLE_DEVICES='' pytest tests/ 开始,定位并修复本 PR 引起的 14 个 fixture 问题后,对 3 个需要可见 CUDA 的 checkpoint 执行阶段和 2 个 upstream registry 顺序用例显式 deselect,得到 1307 passed, 17 skipped, 5 deselected

全量回归发现与修复

第一次在 317cc44 clean worktree 中运行 pytest tests/,结果为 1293 passed, 17 skipped, 8 failed, 11 errors。逐项单独重跑并与 upstream/main@5b23011 对比后,结论如下:

  1. 11 个 SGLang router-registration error 由本 PR 触发。sglang_engine.py 新增了 build_mixture_lora_configis_mixture_lora_enabledconfigure_mixture_lora_external_model import,旧测试使用的伪模块没有提供这些符号,导致 fixture 在导入被测模块时失败。87dfe2f 已补齐对应 stub。
  2. 3 个 SFT loss failure 由本 PR 的严格 microbatch 缩放校验触发。旧测试只验证 recompute 参数转发,没有模拟有效的 DP world size,Megatron 未初始化时返回 0。87dfe2f 在该 fixture 中固定 DP world size 为 1,使测试输入满足真实训练前提。
  3. 修复后,SGLang router-registration 与 SFT chunked CE 两组共 44 个测试全部通过。
  4. 3 个 Mixture checkpoint 用例在 CUDA_VISIBLE_DEVICES='' 下无法执行,因为当前 Megatron distributed checkpoint 实现即使保存 CPU tensor,也会在 finalize 阶段调用 torch.cuda.current_device()。它们随后已在 GPU 可见环境全部通过。
  5. 2 个 registry identity failure 是 upstream 基线中的测试顺序问题。test_registry_reinforce_plus_plus.py 会删除并重新导入 relax.core.registry,导致后续已收集测试持有旧 enum 引用。相同命令已在未包含 Task 25 改动的 upstream/main@5b23011 复现,本 PR 不修改该无关问题。

最终在 87dfe2f clean worktree 中重跑完整 CPU 可运行测试集,显式 deselect 上述 3 个需要可见 CUDA 的用例和 2 个 upstream 基线用例,结果为:

1307 passed, 17 skipped, 5 deselected, 74 warnings in 407.28s

随后在 8×A800 可见 GPU 环境运行完整测试集,结果为:

1312 passed, 15 skipped, 2 failed in 448.80s

两个失败为 registry enum identity/order 问题,已使用相同命令在 upstream/main@5b23011 复现;Task 25 新增的 3 个 CUDA checkpoint 用例全部通过。

兼容性与边界

  • N=1 或不设置 --lora-num-experts 时不创建 router,不启动 external model,继续走现有单 LoRA 注入、导出和 rollout 路径。
  • Mixture 首版仅在 Qwen3 的 linear_qkvlinear_proj 上启用;其他模型和 projection 不会静默套用。
  • 当前训练执行器为 dense,checkpoint 和 transport schema 不记录执行器类型。
  • rollout 使用相同路由公式,但属于推理执行;训练端 grouped sparse kernel 不在本 PR 范围内。
  • DP size 可在恢复时变化;TP/PP 变化需要保持与 checkpoint shard 布局和当前 Megatron 恢复能力一致。

建议重点审查

  1. qkv/proj 在 TP/SP 下的参数 shape、collective 顺序和 router 梯度归约是否符合 Megatron 契约。
  2. response mask、microbatch 缩放、CP 聚合和 PP stage 本地 aux loss 是否会重复或漏算梯度。
  3. site_id、state spec、transport spec 和 checkpoint metadata 是否足够稳定且能在不一致时立即报错。
  4. 首次和后续权重同步是否只发送预期参数,并且不会在失败时提前更新 rollout weight version。
  5. N=1 路径是否完全绕开 Mixture model、checkpoint 和 SGLang external model。
  • 全仓库 pre-commit hooks 通过(离线环境中单独执行 Gitleaks tracked-files hook)
  • 完整 CPU 可运行测试集通过(1307 passed, 17 skipped, 5 deselected
  • GPU 可见的 pytest tests/ 已运行(1312 passed, 15 skipped, 2 failed;2 个失败均在 upstream 复现)
  • 最新 main 合并版本测试通过(全量 pytest、Task 25 Megatron CUDA 61 passed、S3 loader 51 passed
  • 已增加新测试
  • 已更新文档

验证环境说明:远端训练容器为离线环境,无法访问 GitHub 等外部地址。直接执行 pre-commit run --all-files 时,pre-commit 会尝试从 GitHub 初始化 Gitleaks hook 仓库,并停在网络连接阶段。为完成同等范围的离线检查,本次先使用 SKIP=gitleaks pre-commit run --all-files --show-diff-on-failure 运行其余全部 hook,再调用仓库自带的 .pre-commit-hooks/gitleaks_tracked.py 和服务器已安装的 Gitleaks v8.24.2 扫描全部 tracked files。两部分均成功退出,Gitleaks 结果为 no leaks found,检查后 clean worktree 无改动。

变更类型

  • Bug 修复
  • 新功能
  • Breaking change
  • 文档更新
  • 无功能变化的重构
  • 性能优化
  • CI/CD 或构建修改

截图与日志

200-step 训练稳定性

task25-training-stability

图注:Qwen3-4B DAPO GRPO 使用 BF16 连续完成 200 step,全部 scalar 保持有限值,最后 50 step 的 raw reward 均值为 0.3475。

Expert 路由

task25-expert-routing

图注:最后 50 step 的全局 expert 激活权重为 [0.2502, 0.2457, 0.2501, 0.2540],72 个 routed site 中单个 expert 的最大平均权重为 0.4110。

显存与吞吐

task25-performance

图注:Mixture 实验采样到的单卡峰值显存为 64,084 MiB,最后 50 step 平均耗时为 234.19 秒/step。

Checkpoint 恢复

iteration-50 快照已完成真实续训:完整 distributed checkpoint 和数据状态从 iteration 49 恢复,随后完成 step 50–52。命令、验收点和日志摘要见 followup/02_断点续训.md

全参、单 LoRA 与 Mixture 对比

perf-curves

图注:三组 step 2–11 的逐步耗时、rollout 吞吐和 actor train 吞吐;同一步下三组平均 response 长度相差不到 0.3%。

perf-window-means 图注:统一窗口内的 step time、rollout time、actor train time、两项吞吐和平均 response 长度对比。 perf-overhead-breakdown 图注:Mixture 相对单 LoRA 的变化及额外 step time 分解;当前 dense Mixture 的 step time 增加 28.56%,额外时间主要来自 rollout。

Actor 阶段与端到端显存对比

图注:左图展示 step 2–11 的 actor 阶段单卡系统显存均值变化,中图汇总均值、P95 和峰值,右图对比 actor 阶段峰值与端到端峰值。三组统一设置 --sglang-mem-fraction-static 0.7,端到端峰值主要由 SGLang rollout 静态显存池、KV cache 和生成 batch 决定,因此数值接近;单 LoRA 和 Mixture-LoRA 的 actor 阶段显存均值比全参分别低 30.3% 和 23.8%。

单 LoRA 对照所用镜像未应用 Relax 的 Megatron-Bridge PEFT 补丁,因此缺少 create_peft;本次实验临时使用镜像中已有的 LoRA 类完成构建。该兼容修改未纳入本 PR,使用仓库要求的 docker/patch/megatron/20260506-85bced0ae.patch 后无需回退。

pophirasawa and others added 22 commits August 9, 2026 05:37
#### Summary

为 Mixture-of-LoRA 提供训练端和 rollout 端可复用的路由基础逻辑。本次提交只增加公共路由能力和 CPU 测试,不修改现有单 LoRA 路径。

#### Changes

新增 RoutingDecision 和 RoutingStatistics,统一保存 Top-K 前概率、expert 编号、归一化激活权重,以及可跨 rank 聚合的逐 expert 原始统计。实现 FP32 softmax、token-level Top-K 路由、response mask 过滤、归一化熵和 balance loss;expert 选择份额按 valid_tokens * K 归一化,离散选择统计不参与反向传播,router 梯度通过 Top-K 前概率计算。新增 17 个 CPU 测试,覆盖多个 K、temperature、mask、空 response、梯度、entropy 和非法参数。

#### Verification

pytest:17 passed。Ruff、docformatter、通用文件检查和冲突标记检查均通过。gitleaks hook 因 H20 访问 proxy.golang.org 时 TLS 超时,未能完成环境安装;失败发生在依赖下载阶段,并非扫描发现问题。
#### Summary

完成 Mixture-of-LoRA 阶段 A 的公共配置、参数 schema 和无参数执行器接口,为 Megatron 与 SGLang 后续实现提供同一组结构和数学定义。

#### Changes

新增 MixtureLoraConfig 及 N、R、K、temperature、aux loss、alpha 和目标层校验;固定 expert A/B 与 router 的参数名称和全局 shape;新增 checkpoint/transport 共用的状态与 TP 分片描述;新增显式并行上下文、RoutedLoRAExecutor 协议和纯 PyTorch dense executor;增加逐 site balance loss 平均函数,并保证执行器不持有参数、不修改路由结果。

#### Verification

Mixture-of-LoRA L0 测试 38 passed,覆盖 FP32、FP16、BF16、输出、梯度、mask、schema、可重复性和异常输入。现有单 LoRA 回归 43 passed、5 skipped。全仓 pre-commit 全部通过,包含 Ruff、docformatter 和 gitleaks。
#### Summary

接入 Mixture-of-LoRA 命令行参数和启动阶段校验,并保持单 expert 配置继续使用现有 LoRA rollout 路径。

#### Changes

新增 expert 数量、router Top-K、temperature 和 balance loss coefficient 参数;N 大于 1 时要求完整 router 配置、colocate、SGLang TP/DP 为 1,并拒绝 fully-async、merge mode、adapter mode 和不支持的目标层;新增 Mixture 启用判断和共享配置构造;将旧 LoRA rollout mode 校验集中到独立函数,N=1 仍自动选择现有 merge 路径。

#### Verification

参数、PEFT 和单 LoRA 回归共 101 passed、5 skipped。全仓 pre-commit 全部通过,包含 Ruff、docformatter 和 gitleaks。
Summary:
- 接入 Megatron 训练端的 Mixture-of-LoRA 核心模块和 Bridge PEFT 注入路径。
- 保持单 expert 配置继续使用原有 LoRA 实现。

Changes:
- 新增打包的 expert A/B 参数、FP32 token router、dense Top-K 执行和线性层包装器。
- 在模型 provider 中按 expert 数选择实现,并在 optimizer 建立前检查 base 冻结及 expert/router 可训练状态。
- 扩展参数分类与分项统计,保持 base state key 和 Megatron 线性层返回协议。
- 增加 CPU 数值、梯度、初始化、state key、Bridge 匹配、provider 路径和 CUDA profiler 测试。

Verification:
- 相关 pytest:137 passed, 5 skipped。
- H20 CUDA forward/backward 与 profiler 测试通过。
- pre-commit run --all-files 全部通过。
Summary:
- 为每个训练 microbatch 建立 Mixture-of-LoRA 路由上下文。
- 将逐 site balance loss 直接附着到模型激活,保持 policy loss 标量不变。

Changes:
- 使用 full_loss_masks 过滤 prompt、padding 和 dummy token,并处理 batch-first mask 与 Megatron 激活布局。
- 普通 loss、per-token loss 和动态 batch 共用同一个 microbatch 缩放 helper。
- 按全模型 site 数归一化 aux loss,分离可导 aux tensor 与无梯度路由记录。
- 捕获 activation checkpoint 的路由上下文,并补齐冻结 base 时的 recompute input-grad 路径。
- 增加 mask、缩放、dummy、router 梯度、checkpoint 恢复、真实 Bridge 和真实 ColumnParallelLinear 测试。

Verification:
- 相关 pytest:152 passed, 8 skipped。
- 真实 Megatron/Bridge 定向测试:2 passed。
- H20 CUDA profiler 测试通过。
- pre-commit run --all-files 全部通过。
Summary:
- 在 optimizer step 结束后汇总每个 Mixture-of-LoRA site 的路由统计并接入现有训练日志。
- 保留实际训练目标中的 aux loss,同时输出用于判断 expert 塌缩的逐 site 与全局指标。

Changes:
- 按固定 site_id 表打包 Top-K 前概率、Top-K 后权重、选择次数、Top-1 次数、熵和有效 token 数。
- 先在 DP/CP group 汇总原始统计,再在 pipeline group 合并各 stage 持有的 site。
- 输出 expert 平均概率、平均激活权重、选择份额、Top-1 比例、归一化熵、balance loss 和实际 aux loss。
- 使用训练目标权重汇总 balance loss,支持普通 loss、per-token loss 和 dummy microbatch。
- activation recompute 使用相同 key 覆盖记录,step 聚合完成后清理上下文。
- 增加手算指标、空记录、recompute 去重及两进程 DP/PP 聚合测试。

Verification:
- 相关 pytest:108 passed, 2 skipped。
- 真实 Megatron/Bridge 定向测试:2 passed。
- 真实 Megatron/Bridge 环境导入训练模块成功。
- pre-commit run --all-files 全部通过。
Summary:
- 为 Mixture-of-LoRA 增加 qkv 与 proj 的 Tensor Parallel、Sequence Parallel 执行路径。
- 保持 TP 分片后的 forward、expert/router 梯度和 aux loss 与单卡数学参考一致。

Changes:
- qkv 的 A 沿 rank 维分片,B 沿输出维分片,router 在 TP rank 间复制并同步梯度。
- proj 的 A/router 沿输入维分片,B 沿输出维分片,显式汇总低秩中间结果和 router logits。
- 增加 sequence gather/scatter 及其反向通信,所有 collective 显式使用构造时保存的 TP group。
- qkv aux loss 按 TP size 缩放后再同步 replicated router 梯度;路由指标仅由 TP rank 0 记录。
- 保留 TP=1 参数形状与原有执行结果,并校验 rank、输入和输出维度的可分片性。
- 增加两进程 qkv/proj、SP 开关、policy gradient、aux gradient、mask 对齐和统计去重测试。

Verification:
- 相关 pytest:109 passed, 2 skipped。
- 两进程 TP/SP gloo 对照测试通过。
- 真实 Megatron/Bridge 定向测试:2 passed。
- H20 CUDA 单卡 profiler 测试通过。
- pre-commit run --all-files 全部通过。

Limitations:
- 当前 H20 节点仅暴露 1 张 GPU,尚未执行 NCCL TP=2 测试。
Summary:
- 将 Mixture-of-LoRA expert、router 和配置接入 Megatron 原生 distributed checkpoint。
- 恢复时校验 Mixture 配置,并保持现有单 LoRA 的 HF PEFT 导出行为。

Changes:
- qkv/proj 分别声明 A、B 和 router 的全局 shape 与 TP 分片轴。
- 以模块 extra state 保存 schema version、site_id、N/R/K/T/C、alpha、target、输入输出维度和 dtype。
- 加载配置不一致时列出具体字段并停止恢复。
- Mixture 模式跳过仅适用于标准单 LoRA 的 _save_lora_to_checkpoint 导出;N=1 路径不变。
- checkpoint 不记录 dense executor 类型,后续执行器可以共享同一参数结构。
- 增加配置冲突、真实 torch_dist 写盘/恢复、参数、路由、输出和 loss 一致性测试。

Verification:
- 相关 pytest:110 passed, 2 skipped。
- 真实 Megatron distributed checkpoint 保存/恢复测试通过。
- 真实 Megatron/Bridge 定向测试通过。
- pre-commit run --all-files 全部通过。
新增 Qwen3 Mixture-of-LoRA SGLang 外部模型,在 qkv_proj 和 o_proj 上复用训练侧 Top-K 路由与 dense executor。

通过启动前 JSON 环境变量传递路由配置,并校验外部模型包冲突、参数名称、shape 和 dtype。补充单 LoRA 兼容、训练与 rollout 数值一致、BF16 router 及权重加载测试。
SGLang PP worker 加载 Mixture 参数时跳过不属于本 stage 的 layer,同时保留本地未知参数的严格报错。

新增 H20 BF16 CUDA Graph capture 测试和 PP layer 过滤测试,确认 routed adapter 可以沿用现有 graph 执行路径。
新增 Mixture-of-LoRA 权重收集器,按固定 schema 汇总 PP/TP 分片,并将 Megatron 的 GQA 分组 QKV 排布转换为 SGLang 的 Q/K/V 连续排布。

首轮同步发送 base、全部 expert 和 router,后续只发送 expert 和 router;只有最后一个 routed chunk 携带新 weight version。复用现有 pause、flush、IPC update 和 continue 流程,并从 raw/Bridge HF iterator 中排除 Mixture 参数。

补充 QKV 转换、HF 过滤、首轮与后续批次、CPU mirror、生命周期顺序和单 LoRA 回归测试。
SGLang 按 Hugging Face checkpoint 的 architecture 名称注册外部模型。本次将入口类名改为 Qwen3ForCausalLM,确保 Mixture-of-LoRA 实现覆盖内置 Qwen3,而不是启动后继续使用原模型。

Mixture-of-LoRA 是纯文本模型,启动子进程前会清理多模态外部处理器环境变量,并补充入口注册和环境隔离测试。H20 实机已验证预填充、CUDA Graph 解码及连续三次 router/expert 在线更新。
Mixture-of-LoRA 权重更新按暂停、发送和恢复三个阶段同步各训练 rank 的失败状态。任一阶段失败时恢复 rollout generation,并保持原 weight version 与 base 同步状态,避免服务停在暂停状态或发布半完成版本。

IPC 流水等待上一 chunk 失败时仍先完成既有 chunk barrier,防止其他 rank 永久等待。新增 engine 更新失败和 chunk barrier 失败注入测试;Task 25 回归 173 passed,全量 pre-commit 通过。
静态 CP 下按 site 在 context-parallel group 汇总 Top-K 入选次数、路由概率和有效 response token 数。每个 rank 只保留本地概率和的可导贡献,随后由现有 DP/CP 梯度归约合成完整序列的 balance loss。

普通 loss 与 per-token loss 分别使用正确的样本或全局 token 缩放,路由日志中的 aux loss 不随 CP 数放大。新增 CP=2、空 token rank、梯度和指标参考测试,并在启动阶段明确拒绝首版不支持的动态 CP。Task 25 完整回归通过,全量 pre-commit 通过。
补充 Mixture-of-LoRA checkpoint 恢复后的下一步一致性测试。第一个 optimizer step 后保存 expert/router、AdamW、LR scheduler、iteration 和 Torch RNG,再比较连续训练与恢复训练的第二步结果。

测试启用 dropout 并确认 router 在第二步发生更新;恢复后的 loss、全部参数、优化器动量、scheduler 和 iteration 与不中断路径一致。Task 25 回归 176 passed,全量 pre-commit 通过。
新增 Qwen3-4B、DAPO math、GRPO、8 卡 colocate 的 Mixture-of-LoRA recipe,显式配置 N=4、R=16、Top-K、temperature 和 balance coefficient。训练侧使用 TP=2/SP,rollout 启动八个独立的 TP=1/DP=1 SGLang engine,并保留命令行覆盖参数。

新增中英文使用文档和 VitePress 导航,说明启动条件、支持范围、路由公式、逐 site 指标、权重同步和 checkpoint 恢复。Bash 语法检查及全量 pre-commit 通过。
Summary

修复 Mixture-of-LoRA 在 PP/VPP 训练中的 site 标识、权重转换和 rollout 同步问题,使各 pipeline stage 的 expert 与 router 能稳定聚合并更新到 SGLang。

Changes

- 使用全局层偏移生成稳定 site_id,并传递 virtual pipeline stage。
- base 权重仅由参数所属 stage 执行 Bridge 转换,转换后在 PP group 广播;修正转换耗时统计。
- 汇总各 PP stage 的 Mixture 参数元数据,补充 selective CPU offload 和 SGLang base CPU backup。
- 增加 rank/TP 整除、VLM/MoE 拒绝和 Bridge 可选依赖保护,并增强同步失败日志。
- 补充 PP site、权重同步、offload、参数校验和 rollout 回归测试。

Verification

- Task 25、两卡分布式及单 LoRA 兼容回归:192 passed。
- Bridge 与权重同步定向回归:33 passed。
- pre-commit:除独立执行的 gitleaks hook 外,其余 hook 全部通过。
- Gitleaks v8.24.2 staged scan:no leaks found。
- A800 8 卡 PP=2、TP=2、SP、colocate 两步训练完成,包含两次训练后权重同步和 iteration 0/1 checkpoint。
Summary

对齐 Megatron 原生分布式 checkpoint 设计,使 Mixture-of-LoRA 在保持 TP/PP 不变时支持调整 DP size 后恢复 optimizer、expert、router 和训练进度。

Changes

- recipe 启用 fully reshardable distributed optimizer,并在中英文文档说明跨 DP 恢复条件。
- 补齐 TP 路由指标同步,使日志 rank 在任意 TP rank 上都能获得完整指标。
- 增加 DP 梯度与指标、PP stage 梯度与指标、PP checkpoint 归属及 DP 缩容恢复测试。
- 更新 VPP Mixture factory 回归测试,并让纯 CPU checkpoint 测试不依赖 GPU 空闲显存。

Verification

- Task 25、单 LoRA 及相关 VPP 定向回归:232 passed。
- ruff、冲突标记和 whitespace 检查通过。
- Qwen3-4B、DAPO math、colocate 从 4 进程 DP=2 checkpoint 恢复到 2 进程 DP=1,完成 step 2 并保存 iteration 2。
- 恢复后四个 expert 的全局平均激活权重约为 24.5%、26.2%、22.6%、26.7%,未出现单 expert 塌缩。
将 Qwen3-4B Mixture-of-LoRA recipe 从 FP16 切换为 BF16,并移除仅适用于 FP16 动态缩放的 loss scale 参数。同步更新中英文使用文档,使公开 recipe 与已完成的 200-step 验收实验配置一致。
为 SGLang router registration fixture 补充 Mixture 配置与 external model stub,并为 SFT loss 测试提供有效的 DP world size。修复全量 pytest 中由新增 Mixture import 和严格 loss 缩放校验触发的测试错误。
默认 CPU CI 未安装 Megatron 或 SGLang 时,按仓库现有测试约定跳过依赖真实后端的新增用例,避免在测试收集阶段失败。

安装完整后端的环境仍会执行参数校验、权重同步、SGLang 配置和模型一致性测试。
默认 CPU CI 未安装 Megatron 时,张量并行路由指标用例仍会失败:mp.spawn 拉起的子进程是全新解释器,不加载 pytest 与 conftest,worker 内的 from megatron.core import parallel_state 直接抛 ModuleNotFoundError,并以 ProcessRaisedException 冒泡,使三个 Python 版本的测试 job 全部中断。

在 mp.spawn 之前补上 megatron.core 与 megatron.training 的 importorskip,并为该文件补充缺失的 pytest 导入。守卫模块按 worker 的实际依赖链选取:parallel_state 来自 megatron.core,_reduce_mixture_lora_routing_metrics 所在的 relax.backends.megatron.model 在模块级还需要 megatron.training。

这是 f9ce3fb 守卫补齐的延续,该 commit 覆盖了模块级可见后端导入的四个文件,遗漏了本文件这种藏在子进程里的导入。写法与 test_mixture_lora_checkpoint_distributed.py 现有约定一致,安装完整后端的官方镜像仍会完整执行该用例。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pophirasawa
pophirasawa marked this pull request as ready for review August 11, 2026 17:05
@pophirasawa
pophirasawa marked this pull request as draft August 11, 2026 17:05
@pophirasawa
pophirasawa marked this pull request as ready for review August 11, 2026 17:05
@pophirasawa pophirasawa changed the title Feat/task25 mixture lora Feat/task25 mixture of lora Aug 12, 2026
同步最新 main 的 S3 模型来源、HF/FP8 导出和权重分桶更新。

冲突处理说明:
- 模型保存路径保留上游 patch_megatron_model、FP8 streaming writer 和 post hook,同时让 Mixture 模式跳过仅适用于单 adapter 的 LoRA 导出。
- Tensor 权重同步保留上游不等长 bucket padding,并允许 Mixture 分块传输在最终分块前使用空 weight version。
- SGLang 启动保留上游 ModelSource/S3 逻辑,并在子进程启动前注入 Mixture external model 配置及 base-once 同步所需的 CPU 权重备份。

本提交只解决以上三个代码冲突;S3 loader 测试 fixture 的兼容修复留给后续独立提交。
pophirasawa and others added 12 commits August 12, 2026 23:17
最新 main 新增的 S3 loader 测试会构造精简版 megatron_peft_utils 模块,再单独导入 sglang_engine。Task 25 为 sglang_engine 增加 Mixture 配置与模式判断导入后,原 fixture 缺少对应符号,导致测试在收集阶段失败。

为 fixture 补充 build_mixture_lora_config 和 is_mixture_lora_enabled 两个 stub,并固定返回未启用 Mixture,使测试继续覆盖原有 S3 模型加载路径。该修改只影响测试替身,不改变 S3、SGLang 或 Mixture 的运行时行为。

验证:tests/test_s3_model_loader.py 共 51 个用例全部通过。
SGLang 的 PP 模型使用全局 layer 表,并以占位模块表示其他 stage 的层。Mixture 安装逻辑改为只遍历当前 stage 的 [start_layer, end_layer) 区间,直接使用全局 layer id,避免访问 PPMissingLayer 或重复偏移层编号。

新增 PP stage 回归测试,使用无 self_attn 的占位层验证只为本 stage 真实层安装 qkv/proj adapter,并检查同步 schema 使用正确的全局 site id。

验证:tests/models/qwen3_mixture_lora/test_sglang_model.py,8 passed。
量化 SGLang linear 的首个参数可能是 int32 或 int8 的 packed 权重,不能作为 LoRA expert 和 router 的参数类型。安装 adapter 时继续从 base parameter 获取设备,但优先使用 linear.params_dtype 作为浮点计算类型。

当 linear 无法提供浮点 params_dtype,且首个参数也是整数类型时,在模型构造阶段给出明确错误,避免创建需要梯度的整数 Parameter。

新增量化 linear 测试,覆盖 int32 packed 权重配合 BF16 params_dtype,以及缺少浮点 dtype 时的失败路径。验证:SGLang 模型测试 10 passed。
带版本号的最终分块会在所有 rank 确认前序请求成功后再发送,避免失败更新被发布为完整策略。

同步失败后保持 rollout 暂停并保留原版本,补充故障注入测试及中英文恢复说明。
EP metadata 合并时为 replicated non-expert 参数选择最小全局 rank,保证每个 converted slot 只有一个 owner。

补充 metadata 单测和真实 EP=2 Gloo collective 回归,同时验证不同 expert shard 仍完整保留。
Mixture expert 复用 MCore 的 CPU master-weight 切分和 CUDA model-parallel RNG tracker,避免 qkv rank-axis shard 重复。

新增 CPU Gloo TP2 与 CUDA NCCL TP2 初始化回归,并保留单卡初始化及 LoRA B 零初始化行为。
SGLangEngine 默认关闭 Mixture 自动注入,仅常规 policy rollout actor 显式开启,避免 GenRM 和 teacher 继承 policy adapter。

无 Mixture 配置时清理残留的 router 环境变量,并补充 policy 与辅助 engine 角色隔离测试。
Mixture rollout mapper 尚未定义 MTP adapter site,启动校验在 mtp_num_layers 大于零时直接报告不支持。

补充启用 MTP 的失败测试和 mtp_num_layers=None 的兼容测试。
当前 routing context 只包装 MCore tensor-parallel checkpoint,TE FP8/FP4 full recompute 会绕过 aux-loss 重计算路径。

仅拒绝 FP8/FP4 与 full recompute 的组合,并覆盖 selective 与 BF16 full recompute 的兼容边界。
训练注入和 SGLang rollout mapper 当前只实现 Qwen3 布局,HF 参数校验要求文本 config 的 model_type 为 qwen3。

补充 dense Llama 拒绝与 dense Qwen3 接受测试,并明确既有正向 fixture 的模型类型。
Routing context 使用 qkv_format 区分 bshd 与 thd;bshd 始终将 batch-first mask 转为 sequence-first,避免 B 等于 S 时跳过转置。

补充方阵非对称 mask、packed THD 及既有 DP/PP/CP/TP 路径回归。
SGLang 的 tensor 更新接口按 TP rank 选择序列化 bucket。rollout 使用 TP=1、PP=2 时,两个 pipeline stage 都会读取索引 0,原有 CUDA IPC 句柄只能由首个 stage 所在 GPU 正确打开。

仅对 Mixture-of-LoRA 且 SGLang PP 大于 1 的 colocate 同步改用 host flattened bucket,并临时切换 file_system 共享策略后恢复原值。全参数、单 LoRA 和 PP=1 继续使用原有 CUDA IPC 路径。

增加 host/device 传输与共享策略恢复单测;完成 Qwen3-4B BF16、训练 TP=2、SGLang TP=1/PP=2 的完整 GRPO step,覆盖初始同步、rollout、反向、checkpoint 和训练后同步。
@pophirasawa

pophirasawa commented Aug 13, 2026

Copy link
Copy Markdown
Author

8 月 13 日代码自查及后续修复

8 月 13 日对 Task 25 当前实现b363c18进行了一轮代码自查,发现以下问题。

自查发现的问题

  1. SGLang 在 PP 模式下注入 Mixture 模块时,没有正确处理各 stage 的层范围和全局层号。
  2. SGLang 创建 Mixture adapter 时可能沿用量化 base weight 的 dtype,导致 adapter 使用非浮点类型。
  3. Mixture 权重采用分块同步时,前序分块失败后仍可能发布新版本并恢复 rollout,造成部分更新对外可见。
  4. EP 模式下,复制的非 expert 参数可能被多个 rank 重复转换和发送。
  5. TP 模式下,各 rank 的 LoRA A 初始化没有遵循 MCore 并行线性层的分片和随机数语义。
  6. GenRM 和 managed teacher 可能继承 policy rollout 的 Mixture external model 配置。
  7. Mixture 与 MTP 同时启用时,当前实现没有定义 MTP 层的 adapter 注入和 rollout 映射。
  8. FP8/FP4 与 full recompute 同时使用时,routing context 可能在重计算过程中丢失,导致 aux loss 无法正确回传梯度。
  9. 当前 Mixture 注入和 rollout mapper 只实现了 dense Qwen3,但启动参数没有限制模型范围。
  10. routing context 根据 tensor shape 判断 activation layout,在部分 B=S 或 packed THD 输入下可能错误对齐 mask。

对应修改

问题 提交 修改
1 f2972fe 只在当前 PP stage 的全局层区间注入 Mixture 模块,并修正非首 stage 的层号处理。
2 84550ac 使用 linear 声明的浮点计算 dtype 创建 adapter;无法获得浮点 dtype 时直接报错。
3 c65f640 增加跨 rank 失败确认;前序分块失败时不发布新版本,也不恢复 rollout。
4 43fcc97 为复制的非 expert 参数选择唯一转换来源,各 EP rank 的 expert 参数继续分别保留。
5 acee9fa 使用 MCore 的 CPU master-weight 切分和 CUDA model-parallel RNG tracker 初始化 LoRA A。
6 b7b77f0 仅为 policy rollout 启用 Mixture external model,隔离 GenRM 和 managed teacher。
7 e9abbaf 在启动阶段拒绝当前尚未支持的 Mixture + MTP 组合。
8 06894c7 拒绝 Mixture + FP8/FP4 + full recompute;BF16 full recompute 和 selective recompute 保持可用。
9 39974f8 将当前实现范围限制为 dense Qwen3,其他模型在启动阶段给出明确错误。
10 af1c404 显式传递 bshd/thd activation layout,并按确定规则对齐 mask。

以上问题分别修复并拆成独立提交。修改完成后,我们重新执行了定向测试、全量 pytest,以及 Mixture、单 LoRA、全参数和 PP 场景的短训练。Task 25 的设计与验收范围见 Issue #220

SGLang rollout PP=2

复测时发现 SGLang 的 tensor 更新接口按 tp_rank 读取序列化 bucket。rollout 使用 TP=1, PP=2 时,两个 PP stage 的 tp_rank 都是 0,原来的 CUDA IPC bucket 会让 PP1 尝试读取 PP0 GPU 的句柄,触发 CUDA error: invalid argument

bcf61bd 仅在 Mixture-of-LoRA 启用且 sglang_pp_size > 1 时使用 host flattened bucket,并临时切换 PyTorch file_system 共享策略完成跨进程序列化。共享策略通过 finally 恢复;全参数、单 LoRA 和 SGLang PP=1 继续使用原来的 CUDA IPC 路径。

新增单测覆盖 device/host 两条传输分支以及共享策略恢复。Qwen3-4B BF16 colocate 短训练使用训练 TP=2, PP=1、SGLang TP=1, PP=2,初始权重同步、rollout、反向、checkpoint 和训练后权重同步均通过。

step 0 的全局 expert selection share 为 24.52% / 25.62% / 24.76% / 25.10%,aux loss 为 0.01119,grad norm 为 0.00508

短训练回归

模式 训练并行 Rollout 并行 数据 结果
Mixture TP=2, PP=1 2 个 PP=1 engine DAPO 完成完整 GRPO step,路由、梯度和同步正常
Mixture 训练 PP TP=1, PP=2 2 个 PP=1 engine DAPO 完成完整 GRPO step、checkpoint 和同步
Mixture SGLang PP TP=2, PP=1 1 个 TP=1/PP=2 engine 固定 DAPO 格式 smoke 数据 完成完整 GRPO step、checkpoint 和两次同步
单 LoRA TP=2, PP=1 2 个 PP=1 engine 固定 DAPO 格式 smoke 数据 完成完整 GRPO step、adapter checkpoint 和同步
全参数 TP=2, PP=1 2 个 PP=1 engine 固定 DAPO 格式 smoke 数据 完成完整 GRPO step、checkpoint 和同步

固定 smoke 数据用于控制生成长度和测试耗时,训练、反向和权重同步均为真实执行。Mixture 基线与训练侧 PP=2 另外使用真实 DAPO 数据完成验证。

单 LoRA 短训所用容器的 Megatron-Bridge 缺少 Relax 维护的 create_peft 补丁,运行前按仓库 docker/patch/megatron/20260506-85bced0ae.patch 补齐对应文件。该操作属于镜像环境准备,不在本 PR diff 中。

GitHub CPU CI 修复

上述修改推送后,GitHub Actions 的 Python 3.10、3.11 和 3.12 测试均在同一个 TP 分布式用例失败。acee9fa 将 LoRA A 初始化改为遵循 MCore TP 语义后,相关测试会在运行时导入 Megatron Core;默认 CPU CI 不安装 Megatron,而新增测试遗漏了可选依赖保护。

ad3e145 在三个依赖 MCore TP 初始化的测试入口增加函数级 pytest.importorskip("megatron.core.tensor_parallel.layers")。缺少 Megatron 时只跳过这些 TP 用例,同一文件中的纯 PyTorch 分布式测试仍会执行;完整 Megatron 环境也会继续执行真实的前向、反向和 TP 初始化检查。

默认无 Megatron 环境下,两个 CPU TP 用例为 2 skipped;加载真实 Megatron Core 后,同一批用例为 2 passed。目标文件的 pre-commit 和 git diff --check 均通过。

最终检查

  • pytest -q1686 passed, 15 skipped, 81 warnings,耗时 559.60 秒。
  • Mixture 权重同步定向测试:23 passed
  • pre-commit run --all-files:除单独执行的 gitleaks 外,其余 hook 全部通过。
  • gitleaks 8.24.2:扫描约 9.88 MB,未发现敏感信息。
  • git diff --check:通过。

本轮共增加 12 个独立修复提交,当前最终提交为 ad3e145

TP 初始化回归依赖 Megatron Core 的并行线性层实现,但默认 CPU CI 不安装该可选后端。

在三个 TP 初始化测试入口增加函数级 importorskip:无 Megatron 环境只跳过相关用例,完整训练环境仍执行真实前向、反向和初始化检查。
@GUOGUOPOT

Copy link
Copy Markdown
Contributor

这一系列 commit 完成了 Mixture-of-LoRA 从训练、权重同步到 SGLang rollout的端到端接入,整体工程质量很高。但下面几处问题需要进一步完善,以及几处架构改进建议需要进一步跟进。

代码问题:

  1. relax/backends/megatron/mixture_lora.py:599-631:TP>1 情况下 GPU 初始化走的是 for expert weight in self.lora_A,expert 是切片视图而非 Parameter 本体,而 lora_B 根本没走 affine 初始化;使得 adapter 参数缺少 tensor_model_parallel 属性,导致全局 grad-norm 裁剪被静默算错。
  2. relax/backends/megatron/weight_update/update_weight_from_tensor.py:409-439:逐 rank 的失败同步(412-422 的 all_reduce 判定)只在最终 versioned chunk 边界运行。对每个中间 chunk,ray.get(previous_refs) 的异常只在 ipc_gather_src 那个 rank 上非空,致使中间 chunk 的 IPC 失败会死锁。
  3. relax/backends/megatron/weight_update/mixture_lora_sync.py:273(以及紧邻的 276、281 )是只在 src_rank
    上执行、且发生在 PP/TP collective 之前的 rank-local 检查。如果某个 rank 在这里抛异常,同组其他 rank 会卡死在后面的 collective 上,造成锁死。

架构改进:

  1. relax/utils/mixture_lora.py 和 relax/backends/megatron/mixture_lora.py 同名,建议重新命名点名职责以区分二者。
  2. update_weight_from_tensor.py 里有两套几乎平行的流水线发送循环,其都实现了都实现了"chunk N 的 IPC 与 chunk N+1 的转换重叠"这套流水线,但只有 mixture 那套加了跨 rank 失败同步,这也是之前 bug 死锁根源;建议把"带失败同步的流水线发送"抽成一个 primitive,base 和 mixture 都走它。
  3. relax/models/qwen3_mixture_lora/sglang/model.py 中有许多架构无关的通用机制(如 attach_sglang_mixture_lora / load_sglang_mixture_lora_weights / _routed_linear_forward ),只有最后的 Qwen3ForCausalLM 子类是 Qwen3 专属,它们混在一起将来想支持第二种架构就得复制。建议把通用注入/加载逻辑上提到共享的 sglang mixture 模块,qwen3 模块只留薄薄的子类。

辛苦进一步修复代码问题并适当完善代码架构。

hf_weight_iterator_bridge 在模块导入时执行 from megatron.core import mpu,绑定的是导入那一刻 sys.modules 里的对象。同目录的 test_dtype_codes.py 与 test_broadcast_converted.py 会在各自导入阶段装入 megatron.core 桩模块,所以按目录运行 tests/backends/megatron/weight_update/ 时,bridge 绑定到的是桩 mpu,而用例改写的是 megatron.core.mpu,两者并非同一个对象,两个 bridge 用例因此报 AttributeError;单独运行该文件反而正常。

改为用 patch.object 直接替换 bridge 模块上的 mpu 属性,无论它绑定的是真实模块还是桩模块都能命中。打桩返回值与原先完全一致,只是换了作用目标,未改动任何被测代码。

按目录运行 tests/backends/megatron/weight_update/ 由 73 passed、2 failed 变为 75 passed;单文件运行 23 passed。
relax/utils/mixture_lora.py 与 relax/backends/megatron/mixture_lora.py 同名不同包,导入语句和调用栈里难以一眼分辨,而 SGLang 侧还要再加一个同主题模块,混淆只会更严重。

前者改名为 relax/utils/mixture_lora_common.py,只放后端无关的配置、路由计算、参数命名与传输描述,不允许导入任何训练或推理后端;后者改名为 relax/backends/megatron/mixture_lora_modules.py,只放依赖 Megatron 并行状态的模块与 Bridge 注入。两个文件的模块说明同步写清这条边界,其余改动全部是导入路径同步,没有行为变化。

pytest tests/utils/test_mixture_lora_routing.py tests/backends/megatron/test_mixture_lora.py tests/backends/sglang/test_router_registration.py 为 97 passed。
张量并行开启时,专家权重通过按专家切出的视图逐个初始化。视图与 Parameter 共享存储,数值能写回去,但 Megatron 在视图上设置的 tensor_model_parallel 属性会随视图一起消失。结果是 lora_A、lora_B 以及行并行站点上的 router.weight 都保持默认的未标记状态,梯度裁剪把它们当成各 rank 的重复参数,全局梯度范数只统计其中一份,裁剪系数偏大;行并行与列并行的切分轴不同,这个偏差还会随站点类型变化。

初始化完成后显式给 lora_A、lora_B 打标记,router 在构造时按站点类型打标记:列并行站点的 router 是复制参数,标记为重复;行并行站点的 router 沿输入轴切分,每一份都要计入全局范数。已有标记的参数不重复覆盖。

切分轴此前在适配器初始化、sharded checkpoint 的 axis_map、rollout 权重同步三处各写了一份,容易改一处漏两处。统一收敛到 relax/utils/mixture_lora_common.py 的 mixture_lora_tp_partition_dims,三处都从这张表读取,None 表示复制参数。

新增两个单测覆盖行并行与列并行下三类参数的标记结果;分布式用例补充 param_is_not_tensor_parallel_duplicate 断言,确认切分参数确实参与全局梯度范数。8 卡环境下 tests/backends/megatron 三个 Mixture 分布式用例集 24 passed;CPU 侧 tests/backends/megatron/test_mixture_lora.py 与 tests/utils/test_mixture_lora_routing.py 88 passed,权重同步与 SGLang 用例 29 passed。
权重同步有三处结构相同的流水发送循环:先把上一块的引用交给引擎确认,再发下一块,最后统一等待。等待这一步并不是各 rank 对称的,只有 IPC gather 源那个 rank 持有 object ref,引擎侧出错也只会在它上面抛出。base 与 adapter 两条路径的循环直接裸调 ray.get,完全没有把这个失败告诉别的 rank;Mixture 那条只在发布版本号的最后一块之前做了一次失败标志 all-reduce,中间块的异常仍然是就地抛出。结果是某一块传输失败时,其余 rank 察觉不到,会继续走进下一块的集合通信,一直阻塞到分布式超时,日志里先看到的是超时而不是真正出错的那一块。

新增 relax/backends/megatron/weight_update/synchronized_send.py,提供三个原语:run_synchronized_phase 负责阶段级操作的失败同步,raise_on_any_rank_failure 把任意 rank 的本地异常经 gloo all-reduce 变成全体 rank 一起抛出,send_chunks_pipelined 负责流水发送与确认。update_weight_from_tensor 的三处循环统一改为调用 send_chunks_pipelined,中间块、发布版本号前的确认和收尾的最后一块都经过失败同步;阶段级操作改用 run_synchronized_phase,删除原有的 _run_synchronized_weight_update_phase 与随之不再使用的导入。

成功路径的行为没有变化:发送顺序、张量存活范围和每块的 barrier 都与原来一致。变化的是失败路径,从只有 gather 源 rank 抛出、其余 rank 等待超时,变成任意 rank 出错时全体一起抛出同一个异常。

新增 tests/backends/megatron/weight_update/test_synchronized_send.py 覆盖正常发送顺序、单 rank 失败时全体抛出、以及失败后不再发布后续分块;同步用例中的打桩目标同步指向新模块。
路由权重的形状、dtype 和缺键检查原本写在生成器循环里,与 PP 广播、TP all-gather 交替执行。某个 rank 的分片有问题时,它在广播之前就地抛出异常直接退出,而其余 rank 已经进入同一轮集合通信,只能等到分布式超时才失败,日志里最先看到的是超时而不是那个真正出错的张量,排查成本很高。

改为在任何集合通信之前先完成本 rank 全部分片的校验,并借助上一提交的 raise_on_any_rank_failure 把校验结果广播给所有 rank:只要有一个 rank 校验失败,全体 rank 立即一起抛错,不会有人卡在集合通信上。校验通过后再进入原有的广播与 all-gather 流程,检查逻辑与报错信息保持不变,只是拆成 _collect_local_tensors 与 _iter_weight_chunks 两步。

新增两个单测:一个确认缺键和形状不符会在任何广播、all-gather 发出之前抛错,另一个确认本 rank 分片正常但其他 rank 失败时同样会抛错退出。
relax/models/qwen3_mixture_lora/sglang/model.py 里同时放着两类代码:路由注入、前向改写、权重加载这些对任何架构都一样的逻辑,以及 Qwen3 特有的注入点声明。再接入一个模型架构就得整体复制一遍,两份实现随后各自演进,很快就会出现只修了一边的情况。

架构无关的部分移到 relax/models/mixture_lora_sglang.py:运行时配置读取、路由线性层前向、适配器挂载、路由权重加载,以及供各架构复用的 MixtureLoraSGLangModelMixin(包含 TP 必须为 1 的检查,以及把 mixture_lora 权重从常规权重里分流后再交给父类加载)。qwen3_mixture_lora/sglang/model.py 只剩注入点声明与 EntryClass,代码从 213 行降到 22 行。行为和对外类名保持不变。

测试同步拆分:通用逻辑的用例移到 tests/models/test_mixture_lora_sglang.py,tests/models/qwen3_mixture_lora/test_sglang_model.py 只保留 Qwen3 注入点与注册相关断言。
@pophirasawa

pophirasawa commented Aug 17, 2026

Copy link
Copy Markdown
Author

感谢建议🫡,目前针对 ad3e145 之后 review 提出的 3 个代码问题和 3 条架构改进建议,逐条做了修改,另外自查时发现两个测试问题,一并处理。本轮共 6 个独立提交。@GUOGUOPOT

代码问题

  1. mixture_lora.py 在 TP>1 时逐个初始化 lora_A 的 expert 切片,并将 TP 属性设置在临时切片上。切片销毁后,这些属性也随之丢失;lora_B 同样没有设置 TP 属性。Megatron 因此无法正确判断 adapter 参数的切分方式,可能导致全局梯度范数和梯度裁剪结果不正确。
  2. update_weight_from_tensor.py 只在最后一个权重分块发送完成后同步错误状态。中间分块发送失败时,只有负责发送的训练进程会收到异常,其他进程仍会继续参与后续通信,最终可能因等待超时而退出。全参数和单 LoRA 路径此前也缺少相同的错误处理。
  3. mixture_lora_sync.py 会在 PP 广播和 TP 聚合前检查 Mixture 权重的名称、形状和 dtype,但没有将校验结果同步给其他训练进程。任一进程校验失败后,其他进程仍可能阻塞在后续通信中。

结构改进

  1. relax/utils/mixture_lora.pyrelax/backends/megatron/mixture_lora.py 文件同名,阅读导入语句或报错调用栈时不容易区分两者的职责。
  2. update_weight_from_tensor.py 为全参数、单 LoRA 和 Mixture 分别维护了相近的分块发送循环,但三条路径的错误处理不一致,需要共用一套发送流程。
  3. qwen3_mixture_lora/sglang/model.py 同时包含通用的 Mixture 推理逻辑和 Qwen3 专用代码。继续支持其他模型架构时,通用逻辑容易被重复实现。

对应修改

问题 提交 修改
4 948d38f 将后端无关的工具模块改名为 mixture_lora_common.py,将 Megatron 模块改名为 mixture_lora_modules.py,并同步更新所有导入路径。文件名现在可以直接体现模块职责。
1 e1560ea 直接在 lora_Alora_B 参数上设置 TP 属性,并根据参数所在模块的并行方式记录切分维度。router 参数也按注入位置设置:列并行层中的 router 在各 TP 进程保留完整副本,计算全局梯度范数时只统计一次;行并行层中的 router 按输入维度切分,各进程分别统计自己的切片。lora_B 继续使用 LoRA 原有的零初始化:其值为零,不需要按 TP 切分随机数;改成 affine 初始化会破坏 LoRA 中 B 从零开始的设定,因此这里只补充缺失的 TP 属性。适配器初始化、checkpoint 和 rollout 权重同步改为共用同一套 TP 切分规则。
2、5 a4fdc5d 新增公共权重发送函数,让全参数、单 LoRA 和 Mixture 共用统一的分块发送及错误处理流程。每个分块发送完成后,各训练进程会同步成功或失败状态;任一进程失败时,所有进程都会停止后续发送并报告错误。
3 7c544ce 在 PP 广播或 TP 聚合开始前完成 Mixture 权重校验,并将校验结果同步给所有训练进程。出现参数缺失、形状不匹配或 dtype 不一致时,所有进程都会报告错误并终止本次同步。原有校验内容和错误信息保持不变。
6 2a09100 将通用的路由前向、adapter 挂载和权重加载逻辑移到 relax/models/mixture_lora_sglang.py。Qwen3 文件只保留模型专用的注入位置和入口类,从 213 行缩减至 22 行,对外类名保持不变。

关于问题 2 与建议 5 的改动范围

问题 2 和建议 5 都与权重发送流程有关,因此 a4fdc5d 同时调整了全参数、单 LoRA 和 Mixture 三条路径。三种模式仍沿用原有的权重转换、分块和发送方式,本次只统一发送循环和错误处理。

新的发送函数会在每个分块结束后同步一次执行状态,因此各训练进程必须产生相同数量的分块。现有的两个 HF 权重迭代器均满足这一要求:

  • direct 迭代器按照相同的参数列表和 TP 配置进行分块,各训练进程得到的分块数量一致。
  • Bridge 迭代器在 PP 或 EP>1 时会通过广播补齐转换后的张量;在 TP>1 且 PP=EP=1 时,各训练进程分别转换全量参数。两种情况下,各训练进程得到的张量名称和大小都一致,因此分块数量也一致。

单元测试和三种模式的短训练均验证了这套流程可以正常完成。--update-weight-buffer-size 默认值为 512 MB,4B BF16 模型每次同步约进行 16 次状态同步,这部分开销相对权重传输很小。

pause_generationflush_cachecontinue_generation 等 SGLang 生命周期操作未作调整。

关于问题 1 的 TP 切分规则

处理问题 1 时我们发现,适配器初始化、sharded checkpoint 的 axis_map 和 rollout 权重同步各自维护了一套 TP 切分规则,因此 e1560ea 在补充 TP 属性的同时,将三处统一到 mixture_lora_common.mixture_lora_tp_partition_dims

该函数按注入位置返回每个参数的切分轴,轴的选择与被注入线性层的并行方式一致:

参数 逻辑形状 列并行层(linear_qkv 行并行层(linear_proj
experts.lora_A [E, rank, input] 按 rank 轴切分 按 input 轴切分
experts.lora_B [E, output, rank] 按 output 轴切分 按 output 轴切分
router.weight [E, input] 不切分,各训练进程保留完整副本 按 input 轴切分

列并行层的输入完整、输出被切分,因此 lora_A 沿 rank 轴、lora_B 沿 output 轴切分,router 看到完整输入而保留副本;行并行层的输入被切分,因此 lora_A 和 router 都沿 input 轴切分。lora_B 在两种位置都按 output 轴切分:行并行层会先聚合 lora_A 的部分结果得到完整的中间激活,各训练进程再计算自己的 output 分片并汇总还原为完整输出,与 RowParallelLinear 的输出形态一致,同时让两种位置的显存和计算都均分。保留副本的 router 在初始化时广播对齐,反向传播时对梯度做一次 all-reduce,各训练进程上的取值始终一致。

有三处需要按同一个轴处理这些参数,任何一处与其他两处不一致都不会报错,只会得到错误结果:

  • 参数标记:切分轴写入 Megatron 的 tensor_model_parallelpartition_dim 属性,全局梯度范数据此判断参数是否为 TP 重复参数,直接影响梯度裁剪。
  • sharded checkpoint:axis_map 决定各训练进程的分片如何合并保存,以及改变 TP 大小续训时如何重新切分。
  • rollout 权重同步:需要沿同一个轴聚合各训练进程的分片,还原完整权重后再发送给 SGLang。

统一之后,这三处都从同一个函数读取切分轴,新增参数或调整注入位置时只需修改一处。

自查补充的修改

  • 按目录运行 tests/backends/megatron/weight_update/ 时有两个 Bridge 用例失败,但单独运行对应文件可以通过。测试替换的是 megatron.core.mpu,而被测模块在导入时已经保存了自己的 mpu 引用,因此替换没有生效。1deb023 改为直接替换被测模块中的引用。该提交只修改测试代码,没有改动功能代码和测试数据。

测试

  • 全量 pytest tests/ -q1604 passed, 12 skipped, 82 warnings,耗时 514.74 秒。
  • tests/backends/megatron/weight_update/ 按目录运行:修改前 73 passed, 2 failed,修改后 75 passed;单文件运行 23 passed
  • 本轮改动涉及的用例集合(weight_update、models、sglang mixture、router registration、mixture routing、megatron mixture):210 passed
  • 8 卡环境下三个 Mixture 分布式用例集:24 passed,耗时 245 秒。
  • 新增用例覆盖:行并行和列并行模块中的 TP 参数属性;通信开始前的 Mixture 权重校验及错误同步;分块发送顺序;单个进程失败后的错误同步和发送终止;Megatron 对 Mixture TP 重复参数的判断。

短训练测试

由于本轮修改会影响参数的 TP 属性,以及全参数、单 LoRA 和 Mixture 共用的权重发送流程,我们在最终提交 2a09100 上分别运行了三种 colocate 短训练。三组训练统一使用 Qwen3-0.6B BF16,训练侧配置为 TP=2, PP=1,rollout 侧启动 2 个 TP=1, PP=1 的 SGLang engine,并使用 GRPO 算法、DAPO 奖励和固定测试数据完成 1 个 rollout。

模式 结果
Mixture 完成一个完整的 GRPO step、checkpoint 和两次权重同步
单 LoRA 完成一个完整的 GRPO step、adapter checkpoint 和两次权重同步
全参数 完成一个完整的 GRPO step、全量 checkpoint 和两次权重同步

三组训练均正常结束,update_weights_from_tensor 全部返回 200,权重同步耗时为 3.32 至 3.43 秒。全参数和单 LoRA 路径启用新的错误同步流程后,没有出现通信阻塞或权重同步异常。

Mixture 在 step 0 的四个 expert 平均选中占比分别为 24.88% / 26.45% / 26.35% / 22.32%,top-1 占比分别为 24.68% / 27.90% / 26.45% / 20.98%,aux loss 为 0.01157,top-k 前后的归一化熵为 0.7577 / 0.8200。各 expert 均参与了计算,没有出现路由集中到单个 expert 的情况。固定测试数据只用于控制生成长度和运行时间,前向、反向、checkpoint 和权重同步均按实际训练流程执行。

检查

  • pre-commit run --all-filespre-commit-hooks、Ruff 0.15.9、ruff-format、mdformat、clang-format、docformatter、check-conflict-markersformat-ascii-boxes 全部通过。首次运行时,docformatter 调整了一处 docstring 换行;该修改已并入对应提交,再次运行后全部通过。
  • gitleaks 8.24.2(离线环境下用仓库 gitleaks_tracked.py 包装脚本单独执行):扫描约 9.90 MB,no leaks found
  • git diff --check 通过,工作区中没有检查工具产生的额外修改。

本轮共 6 个独立提交,当前最终提交为 2a09100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants