Skip to content

【Task.33】Reproducibility Bundle - RFC #246

Description

@DreamEnding

RFC 033: Reproducibility Bundle

概要

为Relax训练任务引入轻量级、版本化的实验清单系统。每次训练启动时自动生成一份JSON清单,记录代码版本、解析后参数、硬件环境、训练拓扑等核心信息,用于检查环境差异、对比实验配置和复现运行。采集采用best-effort语义:工具缺失或超时时记录告警,不阻塞训练主流程。

背景

当前问题

RL实验复现困难的根本原因是关键元数据分散:

  • 代码状态不明:未提交的patch、依赖版本漂移、多仓库commit散落
  • 配置丢失:CLI参数、环境变量、YAML配置散布在命令历史、脚本和笔记中
  • 环境不可对比:CUDA/NCCL版本、容器镜像、GPU型号变更无法系统化检查
  • 拓扑信息缺失:TP/PP/CP/EP/DP与节点GPU绑定关系未文档化

这些信息目前分散在:

  • Shell历史和启动脚本
  • configs/env.yaml和运行时环境变量
  • Git仓库(通常含未提交修改)
  • 日志文件片段

缺乏单一、结构化、可机读、可验证的运行记录。

设计目标

为每次训练运行生成一个小型JSON清单(Experiment Manifest),满足:

  1. 完整性:捕获影响结果的关键因素(代码、配置、硬件、拓扑)
  2. 可分享:单文件传递完整上下文,无需外部依赖
  3. 可验证:提供工具检测环境差异
  4. 安全:自动脱敏API密钥、内网IP、私有路径
  5. 兼容性:Schema语义化版本,支持读取旧版清单
  6. 非阻塞:采集限时(4秒)、失败不中断训练

方案设计

系统架构

采用三层设计:

训练入口 (train.py)
    │
    ├─ [Ray初始化前] 采集初始来源
    │       ├─ Git: 多仓库commit、dirty状态、patch
    │       ├─ 配置: 解析后参数、环境变量、配置哈希
    │       ├─ 硬件: GPU型号、CUDA/NCCL版本
    │       └─ 系统: OS、Python、关键依赖版本
    │
    ├─ [Ray初始化]
    │
    ├─ [Ray初始化后] 补充运行时拓扑
    │       └─ Ray: 节点数、角色、资源分配
    │
    └─ 持久化 → <tensorboard-dir>/manifest_<run-id>.json

关键特性

  • 独立采集器,4秒超时保护
  • 递归脱敏所有字符串值(密钥、IP、路径)
  • 原子性写入,100KB大小上限
  • 失败时记录warning,返回None继续训练

Schema结构

版本1.0 JSON文档,语义化版本策略:读取器接受所有1.x,拒绝主版本变更。

{
  "schema_version": "1.0",
  "run_id": "20260805_143025_9fc8093",
  "generated_at": "2026-08-05T14:30:25Z",
  "command": {
    "argv": ["python", "relax/entrypoints/train.py", "--algorithm", "grpo"],
    "cwd": "."
  },
  "code": {
    "relax": {
      "commit": "1c8e2fb98b7af3e1e86263ccd1f375e4d833b217",
      "branch": "Reproducibility-Bundle",
      "dirty": false
    },
    "megatron": {
      "commit": "8325919e7a2f4c3d1b5e8f9a0c2d3e4f5a6b7c8d"
    }
  },
  "config": {
    "resolved_args": {
      "actor_model_path": "<HOME>/models/qwen3-0.6b",
      "tensor_model_parallel_size": 1,
      "global_batch_size": 16
    },
    "selected_env": {
      "CUDA_VISIBLE_DEVICES": "0",
      "NCCL_DEBUG": "INFO"
    }
  },
  "environment": {
    "python_version": "3.12.3",
    "packages": {
      "torch": "2.11.0+cu129",
      "ray": "2.56.0"
    }
  },
  "hardware": {
    "gpu_model": "NVIDIA A100-PCIE-40GB",
    "gpu_memory_gb": 40,
    "cuda_version": "12.9",
    "nccl_version": "2.23.4"
  },
  "runtime": {
    "mode": "single_node_ray",
    "node_count": 1,
    "nodes": [
      {"role": "head", "resources": {"CPU": 48, "GPU": 1}}
    ]
  },
  "training": {
    "algorithm": "grpo",
    "parallel_topology": {"tensor": 1, "pipeline": 1},
    "global_batch_size": 16
  }
}

分区说明

分区 内容
command 脱敏后的argv和归一化工作目录
code Relax/Megatron commit、分支、dirty状态
config 解析后参数、选定环境变量、配置文件哈希
environment Python版本、关键依赖版本
hardware GPU型号/显存、CUDA/NCCL/驱动版本
runtime 本地/单机Ray/多节点Ray,节点角色和资源
training 算法、并行拓扑、batch size

脱敏策略

自动脱敏规则

类型 匹配模式 处理方式
API密钥 *_API_KEY*_TOKEN*_SECRET 保留前缀+***(如wand***
内网IP 10.*.*.*192.168.*.*172.16-31.*.* 替换为<internal-address>
私有路径 /home/用户//lustre/home/用户/ 替换为<HOME>/或移除
内部域名 *.internal*.corp*.local 替换为<internal-host>

环境变量白名单:仅采集以下前缀的变量

  • CUDA_*NCCL_*RAY_*
  • SLURM_*HF_*SGLANG_*

重要性:清单设计为可分享的运维元数据,默认安全脱敏防止意外泄露凭证。

采集实现

每个采集器独立运行,异常不传播:

def _collect_hardware():
    """采集硬件信息,nvidia-smi失败时回退到torch。"""
    try:
        # nvidia-smi → torch API → 部分结果
        return {...}
    except Exception as exc:
        logger.warning(f"硬件采集不完整: {exc}")
        return {}

超时保护

  • 单个采集器:4秒
  • Git操作(patch生成):额外超时保护

失败语义

  • 采集器返回空字典或部分结果
  • 最终清单缺失对应分区,但仍是合法的best-effort记录
  • 训练继续,警告写入日志

集成点

relax/entrypoints/train.py

  1. 参数解析后、Ray初始化前

    • 调用collect_manifest(args)采集初始来源
    • 写入临时文件或内存
  2. Ray初始化后、Controller启动前

    • 调用update_manifest()补充Ray拓扑
    • 原子性写入<tensorboard-dir>/manifest_<run-id>.json
  3. 失败处理

    • 任何阶段失败仅记录warning
    • Controller正常启动,训练不受影响

CLI工具

提供四个子命令(relax/entrypoints/reproducibility.py):

# 手工生成清单(不启动训练)
python -m relax.entrypoints.reproducibility generate --output manifest.json

# 检查环境兼容性(退出码0=兼容,1=不兼容)
python -m relax.entrypoints.reproducibility check manifest.json

# 对比两个清单
python -m relax.entrypoints.reproducibility diff old.json new.json

# 生成重运行脚本(需人工审阅)
python -m relax.entrypoints.reproducibility rerun manifest.json --dry-run

# 验证环境后执行清单记录的命令
python -m relax.entrypoints.reproducibility rerun manifest.json --confirm

check逻辑

  • Git commit完全匹配 → PASS
  • Python/CUDA主版本匹配、次版本差异 → WARN
  • GPU型号不同、主版本不匹配 → FAIL

rerun安全

  • --dry-run仅输出脚本,不执行
  • --confirm检查环境兼容性,拒绝含脱敏参数的命令
  • 不会自动安装依赖或修改Git状态

Schema兼容性

语义化版本策略

  • 次版本升级(1.0 → 1.1):仅新增可选字段,旧读取器忽略新字段
  • 主版本升级(1.x → 2.0):破坏性变更,旧读取器拒绝新文档

读取器行为

  • 接受所有1.x文档
  • 保留未知字段(前向兼容)
  • 规范化旧版字段名(如cli_argscommand.argv
  • 拒绝主版本不为1的文档

实现细节

关键文件

relax/utils/manifest.py              # ~920行:单文件实现
relax/entrypoints/train.py           # 集成钩子(+7行)
relax/entrypoints/reproducibility.py # CLI入口(9行)
tests/utils/test_manifest.py         # 单元测试(463行)
tests/integration/test_manifest.py   # 集成测试(64行)
docs/en/guide/reproducibility.md     # 英文指南
docs/zh/guide/reproducibility.md     # 中文指南
docs/schema/experiment-manifest-v1.schema.json  # JSON Schema

修复的问题

实现过程中发现并修复的生产缺陷:

  1. Ray节点角色检测错误

    • 问题:原逻辑在Ray 2.56真实集群上返回unknown
    • 原因:仅检查IsHeadNode布尔值,Ray 2.56实际使用node:__internal_head__资源标记
    • 修复:同时检查布尔值和资源标记
  2. 非有限浮点数序列化失败

    • 问题:float('inf')导致json.dumps()抛出ValueError
    • 原因:JSON标准不支持Infinity
    • 修复:序列化前转换为字符串"<non-finite:inf>"
  3. 仓库发现失败

    • 问题:cwd不在Relax仓库内时无法找到Git根目录
    • 原因:仅从cwd向上查找.git
    • 修复:先从__file__manifest.py位置)解析,回退到cwd

性能数据

采集耗时(5次运行平均):

  • Git信息:~200ms
  • 硬件探测:~150ms
  • 系统环境:~100ms
  • 总计:~1.5秒(max 2.0秒)

清单大小

  • 无git patch:~2.2 KB
  • 含patch(< 1MB输入):< 100 KB(上限)

训练影响

  • 对数小时运行可忽略(< 0.1%)
  • 失败时无影响(非阻塞设计)

测试覆盖

55个测试,100%通过

单元测试tests/utils/test_manifest.py,43个):

  • 采集器:Git信息、环境变量、硬件探测
  • 脱敏:API密钥、路径、IP、主机名、非有限浮点数
  • Schema:序列化、兼容性、版本拒绝
  • CLI:generate、check、diff、rerun

集成测试tests/integration/test_manifest.py,12个):

  • 端到端采集和持久化
  • 非阻塞失败(写入错误时返回None)
  • 自动触发验证(AST契约检查train.py集成)

验证环境

  • 主机:Windows 11 + Python 3.12
  • 容器:Apptainer SIF(Ray 2.56,无Megatron)
  • 真实Ray集群:本地单节点head

安全考虑

威胁模型

攻击向量

  1. 共享清单时意外泄露凭证
  2. 内部基础设施信息暴露
  3. 恶意清单注入攻击

缓解措施

  1. 默认脱敏:所有敏感键匹配正则自动掩码
  2. 白名单采集:环境变量仅采集安全前缀
  3. 读取器隔离:清单读取器不执行代码,rerun需显式确认

脱敏验证

多层防御

  • 正则表达式模式匹配(密钥、IP、路径)
  • 环境变量前缀白名单
  • 采集后gitleaks扫描
  • 专门测试套件(51个脱敏测试)

测试覆盖

  • API密钥变体(WANDB_API_KEYHF_TOKEN
  • 内网IP(10.0.0.1192.168.1.1
  • 私有路径(/home/user//lustre/home/user/
  • 内部域名(gpu-01.internal*.corp

已知限制

  1. 自由文本无法完全保证:用户自定义参数(如--wandb-notes)理论上可含任意敏感信息,对外发布前仍需人工审查
  2. 采集器语义:best-effort不保证所有环境都能完整采集,容器环境可能缺失部分硬件信息

验收证据

针对Task 33所有验收标准的验证:

标准 状态 证据
版本化schema,向后兼容 v1.0 JSON Schema,1.x兼容测试
支持本地、单机Ray、多节点Ray 主机测试 + Ray 2.56真实探测 + 本地集群
敏感信息默认脱敏 脱敏测试套件 + gitleaks扫描通过
环境差异检测工具 check CLI返回PASS/WARN/FAIL
最小复现工作流 rerun --dry-run + --confirm
不影响训练启动 <2秒,非阻塞失败测试
失败不阻塞训练 模拟写入失败返回None + warning
自动生成 AST契约验证train.py钩子
文档完整 双语指南 + JSON Schema + 文档构建通过

测试命令

# 主机
pytest tests/utils/test_manifest.py tests/integration/test_manifest.py -q
# => 55 passed in 11.13s

# 容器
apptainer exec <SIF> bash -lc 'pytest ... -q'
# => 55 passed in 10.75s

# Pre-commit
pre-commit run --all-files
# => 14 passed, 1 skipped

# 文档
make docs-build
# => 完成于27.82s

与现有系统的关系

TensorBoard:清单存储在TensorBoard日志目录,作为运行元数据的一部分

WandB:清单独立于WandB,未来可选实现清单上传为artifact

日志系统:清单不替代日志,是补充性的结构化元数据

配置管理:清单记录解析后配置的快照,不是配置文件本身

文档位置

  • 中文指南docs/zh/guide/reproducibility.md
  • 英文指南docs/en/guide/reproducibility.md
  • JSON Schemadocs/schema/experiment-manifest-v1.schema.json
  • 实现代码relax/utils/manifest.py

参考

  • JSON Schema Draft 2020-12规范
  • Semantic Versioning 2.0.0
  • OWASP信息泄露防护指南

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions