diff --git a/docs/zh_CN/00-development-architecture.md b/docs/zh_CN/00-development-architecture.md index f6c7b9fce..19e9a3949 100644 --- a/docs/zh_CN/00-development-architecture.md +++ b/docs/zh_CN/00-development-architecture.md @@ -1,6 +1,6 @@ # RL Infrastructure 开发标准 -语言: [English](../en/00-development-architecture.md) | 简体中文 | [日本語](../ja/00-development-architecture.md) | [한국어](../ko/00-development-architecture.md) +语言: 简体中文 UniLab 是一个**高性能、模块化、contract 驱动**的 RL infrastructure 仓库。这个标准只回答一个问题: **什么样的改动是对的**。 diff --git a/docs/zh_CN/01-getting-started.md b/docs/zh_CN/01-getting-started.md index 4faa998b8..812e7b1b1 100644 --- a/docs/zh_CN/01-getting-started.md +++ b/docs/zh_CN/01-getting-started.md @@ -1,6 +1,6 @@ # 快速开始 -语言: [English](../en/01-getting-started.md) | 简体中文 | [日本語](../ja/01-getting-started.md) | [한국어](../ko/01-getting-started.md) +语言: 简体中文 本页只回答三个问题: diff --git a/docs/zh_CN/02-simulation-backends.md b/docs/zh_CN/02-simulation-backends.md index 374291ccc..3d10ed888 100644 --- a/docs/zh_CN/02-simulation-backends.md +++ b/docs/zh_CN/02-simulation-backends.md @@ -1,6 +1,6 @@ # 仿真后端 -语言: [English](../en/02-simulation-backends.md) | 简体中文 | [日本語](../ja/02-simulation-backends.md) | [한국어](../ko/02-simulation-backends.md) +语言: 简体中文 UniLab 当前支持两个仿真后端: diff --git a/docs/zh_CN/03-training.md b/docs/zh_CN/03-training.md index 0ec5bbaf8..fc522da54 100644 --- a/docs/zh_CN/03-training.md +++ b/docs/zh_CN/03-training.md @@ -1,6 +1,6 @@ # 训练指南 -语言: [English](../en/03-training.md) | 简体中文 | [日本語](../ja/03-training.md) | [한국어](../ko/03-training.md) +语言: 简体中文 本页覆盖训练、回放、恢复训练、Hydra override 和 W&B。 diff --git a/docs/zh_CN/04-algorithms.md b/docs/zh_CN/04-algorithms.md index bd5d2ddf7..1010cb5e7 100644 --- a/docs/zh_CN/04-algorithms.md +++ b/docs/zh_CN/04-algorithms.md @@ -1,6 +1,6 @@ # 算法 -语言: [English](../en/04-algorithms.md) | 简体中文 | [日本語](../ja/04-algorithms.md) | [한국어](../ko/04-algorithms.md) +语言: 简体中文 本页只保留算法级说明。入口脚本和通用 CLI 参数见 [03-training.md](03-training.md)。 diff --git a/docs/zh_CN/05-g1-motion-tracking.md b/docs/zh_CN/05-g1-motion-tracking.md index 0a9a3baea..ce300a9f3 100644 --- a/docs/zh_CN/05-g1-motion-tracking.md +++ b/docs/zh_CN/05-g1-motion-tracking.md @@ -1,6 +1,6 @@ # G1 Motion Tracking -语言: [English](../en/05-g1-motion-tracking.md) | 简体中文 | [日本語](../ja/05-g1-motion-tracking.md) | [한국어](../ko/05-g1-motion-tracking.md) +语言: 简体中文 UniLab 当前提供两个 G1 whole-body motion tracking task: diff --git a/docs/zh_CN/06-collaboration.md b/docs/zh_CN/06-collaboration.md index c4f457d8a..439f2ab51 100644 --- a/docs/zh_CN/06-collaboration.md +++ b/docs/zh_CN/06-collaboration.md @@ -1,6 +1,6 @@ # 协作流程 -语言: [English](../en/06-collaboration.md) | 简体中文 | [日本語](../ja/06-collaboration.md) | [한국어](../ko/06-collaboration.md) +语言: 简体中文 仓库文档只记录稳定标准。执行状态、owner 和阶段推进应放在 GitHub 协作对象中。 diff --git a/docs/zh_CN/CONTRIBUTING.md b/docs/zh_CN/CONTRIBUTING.md index d63ffd800..88213198f 100644 --- a/docs/zh_CN/CONTRIBUTING.md +++ b/docs/zh_CN/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to UniLab -语言: [English](../../CONTRIBUTING.md) | 简体中文 | [日本語](../ja/CONTRIBUTING.md) | [한국어](../ko/CONTRIBUTING.md) +语言: 简体中文 ## Development Environment Setup diff --git a/docs/zh_CN/README.md b/docs/zh_CN/README.md deleted file mode 100644 index 9c75ac1fd..000000000 --- a/docs/zh_CN/README.md +++ /dev/null @@ -1,82 +0,0 @@ -![G1 motion tracking overview](../assets/g1_readme.png) - -# UniLab - -语言: [English](../../README.md) | 简体中文 | [日本語](../ja/README.md) | [한국어](../ko/README.md) - -UniLab 的核心目标是验证一个命题: **robot locomotion RL 不需要依赖 GPU 仿真后端**。 - -主流框架常把物理仿真、replay buffer 和策略训练全部耦合在 GPU pipeline 中。UniLab 选择另一条路线: **CPU 仿真 + shared-memory 数据通路 + GPU 训练**。这样既能保持训练吞吐,也能降低对特定仿真器和硬件平台的绑定。 - -```text -┌───────────────────┐ 统一共享内存通路 ┌────────────────────┐ -│ CPU 物理仿真 │ ───────────────────────▶ │ GPU 策略训练 │ -│ mujoco.rollout │ SharedReplayBuffer │ PPO / SAC / TD3 │ -│ 多线程并行 │ (PyTorch shared tensors)│ CUDA / MPS │ -└───────────────────┘ └────────────────────┘ -``` - -- **CPU 仿真**: MuJoCo / Motrix 的 CPU 多线程 step,不需要 GPU 仿真内核 -- **统一内存通路**: collector 和 learner 通过 PyTorch shared tensors 零拷贝通信 -- **GPU 训练**: 策略网络仍在 GPU 上训练,支持 CUDA 和 MPS -- **硬件无关**: macOS 和 Linux 都是一等开发环境 - -## Quick Start - -```bash -# 1. 克隆仓库 -git clone https://github.com/unilabsim/UniLab.git -cd UniLab - -# 2. 安装依赖 -# macOS(MPS,默认安装 PyPI 的 torch wheel) -uv sync - -# Linux 默认(安装 PyTorch 官方 cu128 wheel) -# 需要当前 PyTorch cu128 wheel 所支持的 NVIDIA 显卡与驱动栈 -uv sync - -# 可选: Motrix 后端 -uv sync --extra motrix - -# 3. 运行一个训练任务 -uv run python scripts/train_rsl_rl.py task=go1_joystick -``` - -## Workflow Entrypoints - -| 目标 | 入口脚本 | 默认日志根目录 | -|------|----------|----------------| -| PPO (torch / RSL-RL) | `scripts/train_rsl_rl.py` | `logs/rsl_rl_train//` | -| PPO (MLX, macOS) | `scripts/train_mlx_ppo.py` | `logs/mlx_rl_train//` | -| APPO | `scripts/train_appo.py` | `logs/appo//` | -| SAC / TD3 | `scripts/train_offpolicy.py` | `logs/fast_sac//` / `logs/fast_td3//` | - -训练脚本默认会在训练结束后自动进入回放;设置 `training.no_play=true` 可以跳过。 - -## Repository Map - -- `conf/`: Hydra 配置,以及 task / reward / algorithm 组合 -- `scripts/`: 训练、回放、motion 预处理和工具脚本直接入口 -- `src/unilab/`: 环境、后端、算法和共享工具 -- `tests/`: 单元测试、集成测试和脚本配置测试 -- `docs/`: 四语文档,分别位于 `docs/en/`、`docs/zh_CN/`、`docs/ja/` 和 `docs/ko/` - -## Documentation - -- [00 RL Infrastructure Development Standard](00-development-architecture.md): 设计原则、分层结构、contract 和验证边界 -- [01 Getting Started](01-getting-started.md): 安装、依赖同步、镜像和第一次运行 -- [02 Simulation Backends](02-simulation-backends.md): MuJoCo / Motrix 支持范围与后端选择 -- [03 Training Guide](03-training.md): 训练、回放、恢复训练、Hydra 覆盖参数和 W&B -- [04 Algorithms](04-algorithms.md): APPO、FastSAC 和 FastTD3 的用法与差异 -- [05 G1 Motion Tracking](05-g1-motion-tracking.md): G1 全身 motion tracking 任务说明 -- [06 Collaboration Workflow](06-collaboration.md): GitHub issue / milestone / PR 协作规则 -- [Contributing](CONTRIBUTING.md): 开发流程、测试、CI 和 review 预期 -- [AGENTS](../../AGENTS.md): coding agent 和自动化编辑器在这个 RL infra 仓库中的工作准则 - -## Related Projects - -1. https://github.com/mujocolab/mjlab -2. https://github.com/amazon-far/holosoma -3. https://github.com/google-deepmind/mujoco -4. https://github.com/google-deepmind/mujoco_playground/ diff --git a/scripts/check_docs.py b/scripts/check_docs.py new file mode 100644 index 000000000..5868156bd --- /dev/null +++ b/scripts/check_docs.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +"""Docs validation script for UniLab. + +Validates documentation for: +- Script references (scripts/*.py must exist) +- Hydra config keys (common keys must match expected patterns) +- Relative links (markdown links must point to existing files) +- File paths (paths mentioned in docs must exist) +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import Any + +# Known valid scripts +VALID_SCRIPTS = { + "scripts/train_rsl_rl.py", + "scripts/train_mlx_ppo.py", + "scripts/train_appo.py", + "scripts/train_offpolicy.py", + "scripts/play_interactive.py", + "scripts/visualization_env.py", +} + +# Known valid Hydra config keys +VALID_HYDRA_KEYS = { + "task", + "algo", + "training", + "reward", + # algo subkeys + "num_envs", + "max_iterations", + "num_steps_per_env", + "learning_rate", + "entropy_coef", + "desired_kl", + "load_run", + # training subkeys + "play_only", + "no_play", + "load_run", + "logger", + "sim_backend", + "play_env_num", + "play_steps", + "cam_distance", + "cam_elevation", + "cam_azimuth", + "num_timesteps", + "num_gpus", + "device", + "wandb_project", + "wandb_entity", + "wandb_group", + "wandb_name", + "wandb_tags", + "wandb_notes", + "wandb_mode", +} + +# File patterns to check +DOC_PATTERNS = [ + "*.md", + "docs/**/*.md", + "src/**/*.md", +] + +# Patterns to skip (generated, vendor, etc.) +SKIP_PATTERNS = [ + r"\.git", + r"\.venv", + r"__pycache__", + r"\.pytest_cache", +] + + +def should_skip(path: Path) -> bool: + """Check if path should be skipped.""" + path_str = str(path) + for pattern in SKIP_PATTERNS: + if re.search(pattern, path_str): + return True + return False + + +def find_docs(root: Path) -> list[Path]: + """Find all documentation files.""" + docs: list[Path] = [] + for pattern in DOC_PATTERNS: + if pattern.startswith("*/"): + # Handle recursive patterns + for path in root.rglob(pattern[3:]): + if not should_skip(path): + docs.append(path) + else: + for path in root.glob(pattern): + if not should_skip(path): + docs.append(path) + return docs + + +def check_script_references(content: str, doc_path: Path, root: Path) -> list[str]: + """Check that script references point to existing files.""" + errors: list[str] = [] + + # Pattern: scripts/xxx.py or uv run python scripts/xxx.py + script_pattern = r"(?:uv run python |python )?(scripts/\w+\.py)" + for match in re.finditer(script_pattern, content): + script_path = match.group(1) + full_path = root / script_path + if not full_path.exists(): + errors.append(f"{doc_path}: Script not found: {script_path}") + + return errors + + +def check_file_paths(content: str, doc_path: Path, root: Path) -> list[str]: + """Check that file paths in code blocks exist.""" + errors: list[str] = [] + + # Pattern: `path/to/file` or 'path/to/file' in code-like contexts + # Look for paths that look like source files + path_patterns = [ + r"`(src/[^`]+)`", + r"`(conf/[^`]+)`", + r"`(tests/[^`]+)`", + r"\(src/[^\)]+\)", + r"\(conf/[^\)]+\)", + r"\(tests/[^\)]+\)", + ] + + for pattern in path_patterns: + for match in re.finditer(pattern, content): + rel_path = match.group(1) + full_path = root / rel_path + # Skip if it looks like a pattern or has wildcards + if "*" in rel_path or "<" in rel_path: + continue + if not full_path.exists(): + errors.append(f"{doc_path}: Path not found: {rel_path}") + + return errors + + +def check_markdown_links(content: str, doc_path: Path, root: Path) -> list[str]: + """Check that markdown relative links point to existing files.""" + errors: list[str] = [] + + # Pattern: [text](path/to/file.md) or [text](../path/to/file.md) + link_pattern = r"\[([^\]]+)\]\(([^)]+)\)" + + for match in re.finditer(link_pattern, content): + link_text = match.group(1) + link_target = match.group(2) + + # Skip external links + if link_target.startswith(("http://", "https://", "#", "mailto:")): + continue + + # Resolve relative to document + if link_target.startswith("/"): + full_path = root / link_target.lstrip("/") + else: + full_path = doc_path.parent / link_target + + # Remove anchor + full_path_str = str(full_path).split("#")[0] + full_path = Path(full_path_str) + + if not full_path.exists(): + errors.append(f"{doc_path}: Link not found: {link_target} (text: '{link_text}')") + + return errors + + +def check_hydra_keys(content: str, doc_path: Path, root: Path) -> list[str]: + """Check Hydra config keys for common patterns.""" + errors: list[str] = [] + + # Pattern: key=value or key.nested=value + # Look for patterns like algo.num_envs=2048 + hydra_pattern = r"(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)=" + + for match in re.finditer(hydra_pattern, content): + full_key = match.group(1) + # Split by dots and check each component + parts = full_key.split(".") + + # Skip if the first part is a known top-level key + if parts[0] not in VALID_HYDRA_KEYS and parts[0] not in {"python", "uv"}: + # Only warn for what look like config keys (lowercase with underscores) + if re.match(r"^[a-z_]+$", parts[0]): + errors.append(f"{doc_path}: Unknown config key: {full_key}") + + return errors + + +def check_argparse_vs_hydra(content: str, doc_path: Path, root: Path) -> list[str]: + """Check for outdated argparse style commands in Hydra scripts.""" + errors: list[str] = [] + + # Pattern for old argparse style flags that should be Hydra style + old_flags = [ + (r"--task\s+", "Use Hydra style: task="), + (r"--env_num\s+", "Use Hydra style: algo.num_envs="), + (r"--play_only\b", "Use Hydra style: training.play_only=true"), + (r"--no_play\b", "Use Hydra style: training.no_play=true"), + (r"--load_run\s+", "Use Hydra style: training.load_run="), + (r"--cam_distance\s+", "Use Hydra style: training.cam_distance="), + (r"--cam_elevation\s+", "Use Hydra style: training.cam_elevation="), + (r"--cam_azimuth\s+", "Use Hydra style: training.cam_azimuth="), + ] + + # Scripts that use Hydra (not argparse) + hydra_scripts = [ + "train_rsl_rl.py", + "train_mlx_ppo.py", + "train_appo.py", + "train_offpolicy.py", + ] + + for pattern, message in old_flags: + for match in re.finditer(pattern, content): + start = max(0, match.start() - 100) + end = min(len(content), match.end() + 50) + context = content[start:end] + # Only flag if context contains a Hydra script + is_hydra_context = any(script in context for script in hydra_scripts) + if is_hydra_context: + errors.append( + f"{doc_path}: Outdated argparse flag '{match.group(0).strip()}': {message}" + ) + + return errors + + +def check_document(doc_path: Path, root: Path) -> list[str]: + """Run all checks on a single document.""" + errors: list[str] = [] + + try: + content = doc_path.read_text(encoding="utf-8") + except Exception as e: + return [f"{doc_path}: Error reading file: {e}"] + + errors.extend(check_script_references(content, doc_path, root)) + errors.extend(check_file_paths(content, doc_path, root)) + errors.extend(check_markdown_links(content, doc_path, root)) + errors.extend(check_hydra_keys(content, doc_path, root)) + errors.extend(check_argparse_vs_hydra(content, doc_path, root)) + + return errors + + +def main() -> int: + """Main entry point.""" + root = Path(__file__).parent.parent + docs = find_docs(root) + + all_errors: list[str] = [] + + print(f"Checking {len(docs)} documentation files...") + + for doc_path in docs: + errors = check_document(doc_path, root) + all_errors.extend(errors) + + if all_errors: + print(f"\nFound {len(all_errors)} issue(s):") + for error in all_errors: + print(f" - {error}") + return 1 + else: + print("\nAll documentation checks passed!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/unilab/envs/manipulation/inhand_rot_allegro/README.md b/src/unilab/envs/manipulation/inhand_rot_allegro/README.md index 169365ee9..f902a9712 100644 --- a/src/unilab/envs/manipulation/inhand_rot_allegro/README.md +++ b/src/unilab/envs/manipulation/inhand_rot_allegro/README.md @@ -29,7 +29,7 @@ To regenerate it (e.g. after changing the hand model or keyframe): ```bash # From the UniLab root directory -python unilab/envs/manipulation/inhand_rot_allegro/gen_grasp.py +uv run python src/unilab/envs/manipulation/inhand_rot_allegro/gen_grasp.py ``` **Key options:** @@ -44,6 +44,8 @@ python unilab/envs/manipulation/inhand_rot_allegro/gen_grasp.py | `--viewer` | off | Open live MuJoCo viewer (use with small `--num_envs`) | | `--record_video` | off | Save a preview video of the collection | +*Note: `gen_grasp.py` uses argparse; all other training scripts use Hydra.* + The output is a `(N, 23)` float32 array: `[hand_qpos(16), ball_pos(3), ball_quat(4)]` @@ -53,21 +55,27 @@ The output is a `(N, 23)` float32 array: ```bash # From the UniLab root directory -python scripts/train_rsl_rl.py --task AllegroInhandRotation +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation ``` -**Common options:** +**Common options (Hydra syntax):** ```bash # Override number of environments -python scripts/train_rsl_rl.py --task AllegroInhandRotation --env_num 8192 +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation algo.num_envs=8192 # Resume from a previous run -python scripts/train_rsl_rl.py --task AllegroInhandRotation \ - --load_run 2026-03-10_22-50-13 +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation \ + training.load_run=2026-03-10_22-50-13 # Train without rendering a play video afterwards -python scripts/train_rsl_rl.py --task AllegroInhandRotation --no_play +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation training.no_play=true + +# Use MuJoCo backend (default) +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation training.sim_backend=mujoco + +# Use Motrix backend +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation training.sim_backend=motrix ``` Training logs are saved to `logs/rsl_rl_train/AllegroInhandRotation//`. @@ -77,15 +85,15 @@ Training logs are saved to `logs/rsl_rl_train/AllegroInhandRotation// ### Step 3 — Evaluate / render video ```bash -python scripts/train_rsl_rl.py --task AllegroInhandRotation --play_only +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation training.play_only=true # Custom camera -python scripts/train_rsl_rl.py --task AllegroInhandRotation --play_only \ - --cam_distance 1.5 --cam_elevation -30 --cam_azimuth 45 +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation training.play_only=true \ + training.cam_distance=1.5 training.cam_elevation=-30 training.cam_azimuth=45 # Load a specific run -python scripts/train_rsl_rl.py --task AllegroInhandRotation --play_only \ - --load_run 2026-03-10_22-50-13 +uv run python scripts/train_rsl_rl.py task=AllegroInhandRotation training.play_only=true \ + training.load_run=2026-03-10_22-50-13 ``` The video is saved as `play_video.mp4` inside the loaded run directory. @@ -127,7 +135,7 @@ inhand_rot_allegro/ Key parameters are in `rotation.py` (`RewardConfig`, `DomainRandConfig`) and `unilab/config/manipulation_params.py` (`ppo_config`). -**PPO hyperparameters** (in `manipulation_params.py`): +**PPO hyperparameters** (configured via `conf/ppo/task/allegro_inhand.yaml` and `conf/ppo/reward/allegro_ppo.yaml`): | Parameter | Value | |---|---|