执行摘要
- 一句话:全项目切换到 uv 统一管理多后端依赖与安装流程
- 推荐动作:值得精读,这是 verl 包管理架构的里程碑级迁移。重点看三处设计:manage_envs.py 的 universal lock + conflicts 方案(一个锁文件、运行期 materialize 一个无冲突组合)、prefetch 与 Docker 镜像“先烘焙缓存、运行时离线 sync”的配合、以及用 check_uv_gpu_only.py 静态分析强制 GPU/NPU 使用路径分离的做法。建议同时关注后续对 trtllm / veomni(cu12.9 世界)恢复解锁的演进,以及
DEVICE:-gpu 默认值对 NPU 用户的隐性要求。
功能与动机
PR body 只有一句话:“Support uv with vllm, sglang x fsdp, megatron”,动机需要从实现中还原:此前 verl 依赖 setup.py 动态 optional-dependencies,每个后端一套独立 venv 与 Ray py_executable 切换,缺少统一锁文件,多后端并存时 Docker 镜像无法用一个镜像服务所有 backend。manage_envs.py 的文档串明确说明新设计目标:“verl uses one uv.lock for the whole project ... There is no per-backend lockfile and no Ray py_executable switching”,并用 prefetch 让镜像预烘焙 uv 缓存、运行时再离线 sync 出目标组合。同时,check_uv_gpu_only.py 与 attention_utils.py 的注释揭示了两个配套动机:uv.lock 只解析 CUDA backend(vllm-ascend / sglang-ascend / mindspeed 无法来自 uv),因此 NPU 路径必须保持 ambient python;flash-attn 只发布 CUDA wheel,CPU-only 安装(uv 的 cpu extra、单元测试、开发机)需要纯 torch 等价实现保证 attention 路径可运行。
实现拆解
-
依赖契约重写:pyproject.toml 从 setup.py 动态依赖改为 PEP 621 静态元数据,dynamic 字段移除 optional-dependencies;全部后端拆成 verl-core(公共运行时)+ vllm / sglang / fsdp / megatron / cpu,以及无 torch 依赖的 math / ci / veomni-sft 附加 extra;requires-python 收紧为 >=3.10,<3.13;torch 全家统一 ==2.11.0(cu130),并新增 [tool.uv].conflicts 声明互斥关系,最终提交 5119 行的 uv.lock。flash-attn 与 cupy 通过 flash-attn-cu130torch211、cupy-cu130 子 extra 由父 extra 引用,避免在多个 backend 中重复解析;trtllm(RC sdist 会让 uv lock 失败)与 veomni / nemoautomodel(cu12.9 世界)被注释 defer。
-
新增 manage_envs.py 环境驱动:作为 uv 的统一入口,提供 lock / sync / run / shell / list / clean / prefetch 命令。核心设计有三点:一是命名 venv,--name 或 VERL_VENV_NAME 生成 .venv-<name>,默认 .venv 维护为指向最近一次 sync 组合的符号链接(_point_default_venv 负责重定向,_detach_symlink_target 防止写穿链接);二是 VERL_UV_NO_INSTALL 让内部自建包(如内部 ray / wandb)跳过 uv 的精确同步(--no-install-package + --inexact);三是 prefetch 面向 Docker 镜像构建,先在 uv cache 中烘焙全部后端依赖,镜像运行时再离线 sync 出目标组合。
-
运行期兼容层:verl/utils/attention_utils.py 为 flash_attn.bert_padding 增加 4 个纯 torch 等价实现(_fallback_index_first_axis 等),在 ImportError 时降级;verl/checkpoint_engine/__init__.py 与 base.py 给每个可选引擎模块记录 ImportError 到 CheckpointEngineRegistry._import_errors,get() 报错时直接列出缺失依赖;verl/workers/engine/megatron/utils.py 的 set_random_seed 用 is_cuda_available or is_npu_available 门控 model_parallel_cuda_manual_seed,避免 CPU-only 下报 “Torch not compiled with CUDA enabled”;megatron_checkpoint_manager.py 的 RNG 存取增加 is_device_available() 保护;verl/single_controller/ray/base.py 在 Ray 首次导入前设置 RAY_ENABLE_UV_RUN_RUNTIME_ENV=0,规避 uv 环境下 Ray 的 uv runtime-env hook 处理 working_dir=None 时崩溃;verl/workers/engine/megatron/transformer_impl.py 的 value head 关闭 tie-embeddings 改为直接写 tf_config / provider_overrides,绕开部分 Megatron 构建对 TransformerConfig(**kwargs) 的兼容问题。
-
启动脚本与 CI 门控改造:全部 examples/*.sh、tests/special_e2e/*.sh 的 launch 段统一为 ${VERL_USE_UV:-1} != 0 && ${DEVICE:-gpu} = gpu 双条件 gate:GPU 下用 uv run --frozen --all-packages --extra ... 并把 py_executable 传给 Ray,NPU 回落 ambient python。新增 tests/special_sanity/check_uv_gpu_only.py 静态分析所有 shell 脚本,强制 uv 命令必须位于 GPU gate 的 then 分支、NPU 专属脚本(路径或文件名含 ascend / npu / mindspeed 标记)不得出现 uv;新增 docker/Dockerfile.uv.cu130 配套镜像。
-
测试与文档配套:vllm 相关 update_weights_from_ipc_* 测试用 pytest.importorskip("vllm") 保护并迁移到 vllm.yml 的 venv 中运行;新增 examples/rewards/custome_reward_fn/aime_reward.py,将自定义 AIME data_source 归一化为 aime 后委托 default_compute_score,保证训练(dapo-math)与验证(AIME)走同一 math_dapo 验证器;大量 workflow 与安装文档同步更新。
关键文件:
manage_envs.py(模块 环境管理;类别 source;类型 dependency-wiring;符号 _resolve_name, _venv_dir, _proj_env, _detach_symlink_target): 本 PR 的核心新增文件:universal-lock uv 环境驱动,定义了 backend/conflict 常量、命名 venv 与 .venv 符号链接管理、sync/prefetch 语义,是整个依赖迁移的入口。
pyproject.toml(模块 依赖配置;类别 config;类型 configuration): 依赖契约本体:PEP 621 extras 组织全部后端,声明 [tool.uv].conflicts 与 wheelhouse 路由,requires-python 收紧,是 uv.lock 的输入与 BREAKING 的具体载体。
verl/utils/attention_utils.py(模块 注意力工具;类别 source;类型 dependency-wiring;符号 _fallback_index_first_axis, _fallback_pad_input, _fallback_unpad_input, _fallback_rearrange): CPU-only 兼容的关键补丁:flash-attn 只有 CUDA wheel,新增纯 torch fallback 保证 uv cpu extra、单测与开发机可运行 attention padding 路径。
tests/special_sanity/check_uv_gpu_only.py(模块 脚本检查;类别 test;类型 test-coverage;符号 is_npu_only, strip_comment, uv_command_lines, enclosing_ifs): 新增的静态检查测试:保证所有 shell 脚本中的 uv 命令位于 GPU gate 内、NPU 脚本完全不用 uv,是这次大规模脚本改动的护城河。
verl/checkpoint_engine/base.py(模块 检查点引擎;类别 source;类型 core-logic;符号 record_import_error): 配合可选引擎依赖拆分:新增 record_import_error 与 _import_errors,让缺失传输依赖(cupy / nixl / torch_npu)时能给出明确报错而非笼统的 not registered。
verl/checkpoint_engine/__init__.py(模块 检查点引擎;类别 source;类型 core-logic): 所有可选 checkpoint 引擎模块的 ImportError 从静默吞掉改为记录到 registry,是 base.py 新机制的唯一数据来源。
verl/workers/engine/megatron/utils.py(模块 Megatron 适配;类别 source;类型 dependency-wiring): CPU-only 环境下的 Megatron 种子初始化修复:用 is_cuda_available / is_npu_available 门控 model_parallel_cuda_manual_seed,避免无加速器时报 CUDA 编译错误。
verl/single_controller/ray/base.py(模块 运行时适配;类别 source;类型 entrypoint): uv 集成后 Ray 2.47+ 会自动执行 uv runtime-env hook,导致 working_dir=None 崩溃;此处禁用该 hook(RAY_ENABLE_UV_RUN_RUNTIME_ENV=0),是 uv 下多节点运行的关键修复。
docker/Dockerfile.uv.cu130(模块 部署脚本;类别 infra;类型 infrastructure): 新增的镜像构建文件:配合 manage_envs.py prefetch 在镜像层内烘焙 uv 缓存,使一个镜像离线服务任意 backend 组合。
uv.lock(模块 锁文件;类别 other;类型 configuration): 全项目统一锁文件(5119 行),是本次迁移的产物与后续所有 sync 的解析依据;体积大、冲突解决成本高。
关键符号:_resolve_name, _venv_dir, _proj_env, _detach_symlink_target, _point_default_venv, _pop_name, _require_uv, _expand, _fallback_index_first_axis, _fallback_pad_input, _fallback_unpad_input, _fallback_rearrange, _get_attention_functions, record_import_error, CheckpointEngineRegistry.get, set_random_seed, check_script, enclosing_ifs, is_gpu_gate, compute_score
关键源码片段
manage_envs.py
本 PR 的核心新增文件:universal-lock uv 环境驱动,定义了 backend/conflict 常量、命名 venv 与 .venv 符号链接管理、sync/prefetch 语义,是整个依赖迁移的入口。
# ---- 全局常量:单一 uv.lock 内的所有 backend 组合 ----
# 推理引擎与训练框架各自是 PEP 621 extra,互斥项在 [tool.uv].conflicts 中声明。
# 方案核心:运行时只 materialize 一个无冲突组合进 .venv,所有 Ray worker 共用。
INFERENCE_BACKENDS: list[str] = ["vllm", "sglang"]
TRAINING_BACKENDS: list[str] = ["fsdp", "megatron"]
# DEFERRED(cu12.9 / torch 2.9.1 世界):veomni、nemoautomodel 待上游支持 torch 2.11 后再启用
CU129_BACKENDS: list[str] = []
# cpu 是不带 GPU 依赖的 CI / 单测切片
DEV_BACKENDS: list[str] = ["cpu"]
# 无 torch 依赖的附加 extra,可叠加在任意 backend 组合上,CI 通过 sync <backend...> ci 组合
ADDON_EXTRAS: list[str] = ["math", "ci", "veomni-sft"]
# 互斥集合:同一个 .venv 最多装入每个集合中的一个成员(与 pyproject.toml 的 conflicts 保持一致)
CONFLICT_SETS: list[set[str]] = [
{"vllm", "sglang", "cpu"},
{"fsdp", "cpu"},
{"megatron", "cpu"},
]
def _venv_dir(name: str | None) -> Path:
"""解析本次调用对应的项目 venv 目录。
优先级:用户 --name 指定的 .venv-<name> -> 环境变量 UV_PROJECT_ENVIRONMENT
-> 默认 .venv。所有子命令都通过 UV_PROJECT_ENVIRONMENT 指向同一目标目录,
保证 sync / run / shell / clean / list 语义一致。
"""
if name:
return VERL_DIR / f".venv-{name}"
env = os.environ.get("UV_PROJECT_ENVIRONMENT")
if env:
return Path(env).expanduser().resolve()
return DEFAULT_VENV_DIR
def _proj_env(venv_dir: Path) -> dict[str, str | None]:
"""把 uv 的项目环境变量指向目标 venv 目录。"""
return {"UV_PROJECT_ENVIRONMENT": str(venv_dir)}
def _detach_symlink_target(venv_dir: Path) -> None:
"""若 venv_dir 当前是符号链接则先摘除,让 uv sync 建真实目录。
只有默认 .venv 会成为指向最近一次 sync 命名环境的符号链接,
无命名 sync 时先消费掉旧链接,避免写穿到旧命名环境。
"""
if venv_dir.is_symlink():
prev = os.readlink(venv_dir)
venv_dir.unlink()
print(f"note: {venv_dir} was a symlink (-> {prev}); replacing it with a real env", flush=True)
def _point_default_venv(venv_dir: Path) -> None:
"""让默认 .venv 符号链接指向刚 sync 完成的环境组合。
这样 source .venv/bin/activate 与 CI 无需感知命名环境;重复 sync 指向
同一目标时静默跳过,若 .venv 已是真实目录则告警并保留,绝不删除已物化环境。
"""
link = DEFAULT_VENV_DIR
if venv_dir == link or not venv_dir.exists():
return
if link.is_symlink():
try:
if link.resolve() == venv_dir.resolve():
return # 已指向此处,保持安静
except OSError:
pass
link.unlink()
elif link.exists():
print(
f"warning: {link} is a real directory; not repointing it at {venv_dir.name}. "
f"Activate {venv_dir}/bin/activate directly, or `python manage_envs.py clean` "
"to replace it with a link.",
file=sys.stderr,
)
return
# 同仓库内的命名环境用相对链接(checkout 移动后依然可用),外部路径用绝对链接
rel = os.path.relpath(venv_dir, VERL_DIR)
target = rel if not rel.startswith("..") else str(venv_dir)
link.symlink_to(target, target_is_directory=True)
print(f"pointed {link} -> {target} (latest composition)", flush=True)
verl/utils/attention_utils.py
CPU-only 兼容的关键补丁:flash-attn 只有 CUDA wheel,新增纯 torch fallback 保证 uv cpu extra、单测与开发机可运行 attention padding 路径。
# 纯 torch 实现的 flash_attn.bert_padding 等价函数。
# 动机:flash-attn 只发布 CUDA wheel,CPU-only 安装(单元测试、开发机)没有它,
# 但 verl 的 attention 路径仍要能跑通,只是没有 CUDA 优化。
def _fallback_index_first_axis(tensor: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
"""等价于 flash_attn.bert_padding.index_first_axis:按 indices 取行。"""
assert tensor.ndim >= 2
return tensor[indices]
def _fallback_pad_input(hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int) -> torch.Tensor:
"""等价于 flash_attn.bert_padding.pad_input:把 unpadded 数据按 indices 放回稠密矩阵。"""
other_shape = hidden_states.shape[1:]
output = hidden_states.new_zeros(batch * seqlen, *other_shape)
output[indices] = hidden_states
return output.view(batch, seqlen, *other_shape)
def _fallback_unpad_input(hidden_states: torch.Tensor, attention_mask: torch.Tensor, unused_mask=None):
"""等价于 flash_attn.bert_padding.unpad_input:返回 unpadded 数据、indices 与 cu_seqlens。"""
all_masks = (attention_mask + unused_mask) if unused_mask is not None else attention_mask
seqlens_in_batch = all_masks.sum(dim=-1, dtype=torch.int32)
used_seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32)
indices = torch.nonzero(all_masks.flatten(), as_tuple=False).flatten()
cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0))
return (
_fallback_index_first_axis(hidden_states.reshape(-1, *hidden_states.shape[2:]), indices),
indices,
cu_seqlens,
seqlens_in_batch.max().item(),
used_seqlens_in_batch,
)
def _get_attention_functions() -> tuple[Callable, Callable, Callable, Callable]:
"""按硬件动态加载 attention 工具函数。
优先级:NPU 实现 -> flash_attn CUDA 实现 -> 纯 torch fallback。
新增的 try/except ImportError 分支是本次变更的关键:它让没有 flash-attn
的环境(uv 的 cpu extra)也能 import 本模块并正常执行 pad / unpad。
"""
from verl.utils.device import is_torch_npu_available
global _index_first_axis, _pad_input, _rearrange, _unpad_input
if is_torch_npu_available(check_device=False):
from verl.utils.npu_flash_attn_utils import index_first_axis, pad_input, rearrange, unpad_input
else:
try:
from flash_attn.bert_padding import index_first_axis, pad_input, rearrange, unpad_input
except ImportError:
# flash-attn 仅有 CUDA wheel,CPU-only 环境在这里降级为纯 torch 实现
index_first_axis = _fallback_index_first_axis
pad_input = _fallback_pad_input
rearrange = _fallback_rearrange
unpad_input = _fallback_unpad_input
_index_first_axis, _pad_input, _rearrange, _unpad_input = index_first_axis, pad_input, rearrange, unpad_input
return _index_first_axis, _pad_input, _rearrange, _unpad_input
tests/special_sanity/check_uv_gpu_only.py
新增的静态检查测试:保证所有 shell 脚本中的 uv 命令位于 GPU gate 内、NPU 脚本完全不用 uv,是这次大规模脚本改动的护城河。
# 背景:uv.lock 只解析 CUDA backend(x86_64 Linux / cp312 / cu130),vllm-ascend /
# sglang-ascend / mindspeed 不可能来自 uv。因此所有 shell 脚本里的 uv 命令必须位于
# GPU 专用 if 分支内,NPU 专属脚本则完全禁止 uv——本文件用静态分析强制这两条规则。
# 匹配 uv 调用:裸命令或嵌入字符串(py_executable="uv -v run ...")都算
UV_COMMAND = re.compile(r"""(?:^|[\s;&|(="'])uv\s+(?:-\S+\s+)*(?:run|sync|pip|lock|venv|tool|add|export)\b""")
# GPU gate 的两个必要子句:VERL_USE_UV 开关 + DEVICE 探针
USE_UV_CLAUSE = re.compile(r"\$\{VERL_USE_UV:-1\}")
GPU_CLAUSE = re.compile(r"""\[\s*"\$\{DEVICE:-gpu\}"\s*=\s*gpu\s*\]""")
# NPU 专属目录与文件名标记:这些脚本在任何情况下都不允许调用 uv
NPU_DIRS = ("examples/ascend_extras", "tests/special_npu")
NPU_NAME_MARKERS = ("_npu", "ascend", "mindspeed")
def is_npu_only(rel_path: str) -> bool:
if any(rel_path == d or rel_path.startswith(d + "/") for d in NPU_DIRS):
return True
name = Path(rel_path).name.lower()
return any(marker in name for marker in NPU_NAME_MARKERS)
def strip_comment(line: str) -> str:
"""丢弃整行注释,行尾注释保留:藏在行尾注释后的 uv 命令同样要被标记。"""
return "" if line.lstrip().startswith("#") else line
def uv_command_lines(lines: list[str]) -> list[int]:
return [i for i, line in enumerate(lines) if UV_COMMAND.search(strip_comment(line))]
def enclosing_ifs(lines: list[str], idx: int) -> list[int]:
"""找到包含第 idx 行的 if 行号,由内到外排列。
只有 then 分支内的行才算作被 gate 保护:位于 else / elif 分支的
uv 命令同样需要报错。实现为反向扫描并匹配 fi 深度。
"""
found: list[int] = []
depth = 0
in_else = False
for j in range(idx - 1, -1, -1):
stripped = strip_comment(lines[j]).strip()
if stripped == "fi":
depth += 1
elif depth == 0 and (stripped == "else" or stripped.startswith("elif ")):
in_else = True # 该 if 的 then 分支并不包含当前行
elif stripped.startswith("if ") and not stripped.endswith("fi"):
if depth == 0:
if not in_else:
found.append(j)
in_else = False
else:
depth -= 1
return found
def is_gpu_gate(line: str) -> bool:
return bool(USE_UV_CLAUSE.search(line) and GPU_CLAUSE.search(line))
def check_script(lines: list[str], display: str) -> list[str]:
"""检查单个脚本:所有 uv 命令必须位于 GPU gate 的 then 分支内。
py_executable 字符串透传给 Ray 时也算 uv 调用——如果漏检,worker actor
就会在锁文件并不覆盖的设备上用 uv 启动。
"""
uv_lines = uv_command_lines(lines)
if not uv_lines:
return []
if is_npu_only(display):
return [
f"{display}:{i + 1}: NPU-only script must not invoke uv "
f"(uv.lock covers CUDA backends only): {lines[i].strip()}"
for i in uv_lines
]
errors = []
for i in uv_lines:
gates = enclosing_ifs(lines, i)
if any(is_gpu_gate(lines[j]) for j in gates):
continue # 已被 GPU gate 覆盖,放行
where = (
f"gated only by line {gates[0] + 1} (`{lines[gates[0]].strip()}`)"
if gates
else "not inside a GPU-gated `then` branch"
)
errors.append(
f"{display}:{i + 1}: uv command {where}; it must run inside "
f'`if [ "${{VERL_USE_UV:-1}}" != 0 ] && [ "${{DEVICE:-gpu}}" = gpu ]; then`: {lines[i].strip()}'
)
return errors
评论区精华
评审整体很轻量,最终由 Luosuu 一句 “lgtm :)” 批准。两条实质讨论:一是 codex 自动化评论在 examples/sapo_trainer/run_qwen3_8b_fsdp.sh:126 提出 P2 级问题——${DEVICE:-gpu} 默认值为 gpu,NPU 环境未显式导出 DEVICE 时会误入 uv 分支并把 CUDA 的 py_executable 交给 Ray,建议默认关掉 gate 或先探测设备;合入时该模式被保留(check_uv_gpu_only.py 的正则也以它为基准),属于已接受的取舍。二是 wuxibin89 指出 verl/trainer/constants_ppo.py 中新增的 PYTHONPATH 转发可能与外置 PYTHONPATH 冲突,且已在 PR#7313 修复,作者回应 “OK, the rebase process got error.”,最终该改动在提交 7c7e1f9 中被回退,恢复 main 的实现。
- GPU gate 默认值:NPU 可能误入 uv 分支 (design): 合入时沿用双门控模式(check_uv_gpu_only.py 也以该 gate 为基准),依赖 NPU 用户显式设置 DEVICE=npu;该默认值风险在合入后仍存在,属于已知取舍。
- PYTHONPATH 转发与外部设置冲突 (correctness): 该改动在提交 7c7e1f9 中被回退,恢复 main 分支 get_ppo_ray_runtime_env 的实现(RAY_JOB_CONFIG_JSON_ENV_VAR working_dir 处理),避免与 PR#7313 的修复冲突,问题视为已解决。
- 整体审核节奏与最终批准 (other): 大规模 infra PR 的 review 密度较低,主要技术风险由 codex 自动化评论与合入后的 CI 暴露。
风险与影响
- 风险:
- 全局 BREAKING 安装流程变更:依赖来源从 setup.py 动态解析变为 pyproject.toml 静态 extras + uv.lock,Python 版本上限收紧到 <3.13,torch 统一锁定 2.11.0(cu130),存量环境(Python 3.10/3.11 或 torch 2.9 等)需要整体迁移。
-
多后端强 pin 的升级脆弱性:vllm==0.24.0、sglang==0.5.12、transformers==5.5.3/5.3.0、megatron-bridge==0.5.2 等强版本绑定,任一上游发版节奏变化都会阻塞 verl 依赖升级;69 个 commit 中大量是 retry uv.lock / fix cudnn / fix flash_attn,说明锁文件的手工冲突解决成本很高。
-
NPU 误入 uv 分支风险:cpu 与 uv.lock 只覆盖 CUDA backend,NPU 正确性依赖 DEVICE 环境变量被显式设置;codex 指出的 :-gpu 默认值问题在合入后仍存在,未设 DEVICE 的 NPU 启动会静默拿到 CUDA 环境的 py_executable。
-
attention fallback 语义等价性:attention_utils.py 纯 torch 实现与 flash_attn 版本在极端 mask / 空 batch 场景下行为可能不一致,且 flash-attn 缺失时性能明显下降,但该回退只在 CPU-only 安装中触发。
-
134 文件机械改动回归面:所有 e2e 脚本 launch 段统一改写,单个脚本 gate 笔误会导致 CI 静默使用错误解释器;有 check_uv_gpu_only.py 兜底,但它只做静态文本检查,无法验证运行时解释器选择。
- 影响:对用户/开发者:安装与运行方式改变,必须使用 uv(VERL_USE_UV=0 可回退 ambient python,NPU 路径默认不走 uv);Python 3.12 成为事实上的标准解释器;多后端组合(vllm × fsdp、sglang × megatron)通过 extra 组合在单一 .venv 中实现,取代多 venv 切换。对系统/CI:所有 GitHub workflow 改为 sync <backend...> ci 从锁文件安装;Docker 镜像通过 prefetch 层烘焙 uv 缓存,一个镜像可服务任意后端组合。对团队:依赖升级流程变为“改 pyproject.toml + manage_envs.py lock + 提交 uv.lock”,维护成本转移到锁文件的冲突解决与 wheelhouse 路由上。
- 风险标记:全局 BREAKING 安装流程变更, NPU 误入 uv 分支风险(DEVICE 默认值), uv.lock 大文件手工维护成本高, 多后端版本强绑定,升级窗口受限, 134 文件机械改动回归面大
关联脉络
- PR #7313 (review 中引用的上游修复,标题未提供): wuxibin89 在 review 评论中明确引用:constants_ppo.py 的 PYTHONPATH 转发与外部设置冲突的问题已在 PR#7313 修复,本 PR 最终回退了该处改动。
- PR #7357 [ci] test: migrate workflows from fully_async/one_step_off_policy to v1 separate_async: 与本 PR 都改动了 tests/special_e2e/run_v1_separate_async.sh,本 PR 在其中加入 uv 双门控 launch 段,两条 CI 迁移线在脚本上交叉。
- PR #7293 [ci] chore: Update ci image: 同一 CI/依赖基础设施演进线:本 PR 引入 Dockerfile.uv.cu130 与 prefetch 缓存烘焙,此前的 CI 镜像更新与 NPU 依赖调整为其铺路。
参与讨论