feat(training): release V0.9.1 避障训练与基础策略迁移
This commit is contained in:
@@ -32,7 +32,6 @@ from mjlab.tasks.registry import list_tasks, load_env_cfg, load_rl_cfg, load_run
|
||||
from mjlab.tasks.velocity.mdp import UniformVelocityCommandCfg
|
||||
from mjlab.utils.torch import configure_torch_backends
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
|
||||
from tuning.schema import apply_reward_configuration, validate_configuration
|
||||
|
||||
SCENARIOS = (
|
||||
@@ -61,6 +60,7 @@ class EvaluateConfig:
|
||||
checkpoint: str
|
||||
output: str
|
||||
reward_config: str | None = None
|
||||
task_config: str | None = None
|
||||
num_envs: int = 256
|
||||
steps_per_seed: int = 1000
|
||||
seeds: tuple[int, ...] = field(default_factory=lambda: (101, 202, 303))
|
||||
@@ -163,31 +163,41 @@ def run_evaluation(task_id: str, cfg: EvaluateConfig) -> dict:
|
||||
checkpoint = Path(cfg.checkpoint).expanduser().resolve(strict=True)
|
||||
output = Path(cfg.output).expanduser().resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
per_seed = [_evaluate_seed(task_id, cfg, seed) for seed in cfg.seeds]
|
||||
metrics = {
|
||||
key: fmean(seed_metrics[key] for seed_metrics in per_seed)
|
||||
for key in (*METRIC_NAMES, "fall_rate")
|
||||
}
|
||||
deviations = {
|
||||
key: pstdev(seed_metrics[key] for seed_metrics in per_seed)
|
||||
for key in (*METRIC_NAMES, "fall_rate")
|
||||
}
|
||||
result = {
|
||||
"protocolVersion": 1,
|
||||
"taskId": task_id,
|
||||
"checkpoint": checkpoint.name,
|
||||
"checkpointSha256": _sha256(checkpoint),
|
||||
"seeds": list(cfg.seeds),
|
||||
"numEnvs": cfg.num_envs,
|
||||
"stepsPerSeed": cfg.steps_per_seed,
|
||||
"scenarios": [list(value) for value in SCENARIOS],
|
||||
"metrics": metrics,
|
||||
"metricStd": deviations,
|
||||
"seedMetrics": [
|
||||
{"seed": seed, "metrics": values}
|
||||
for seed, values in zip(cfg.seeds, per_seed, strict=True)
|
||||
],
|
||||
}
|
||||
if task_id == "Unitree-Go2-ObstacleAvoidance":
|
||||
from scripts.evaluate_obstacle import run_obstacle_evaluation
|
||||
checkpoint_hash = _sha256(checkpoint)
|
||||
result = run_obstacle_evaluation(task_id, cfg)
|
||||
if _sha256(checkpoint) != checkpoint_hash:
|
||||
raise ValueError("Checkpoint changed during evaluation")
|
||||
result.update({"taskId": task_id, "checkpoint": checkpoint.name,
|
||||
"checkpointSha256": checkpoint_hash})
|
||||
metrics = result["metrics"]
|
||||
else:
|
||||
per_seed = [_evaluate_seed(task_id, cfg, seed) for seed in cfg.seeds]
|
||||
metrics = {
|
||||
key: fmean(seed_metrics[key] for seed_metrics in per_seed)
|
||||
for key in (*METRIC_NAMES, "fall_rate")
|
||||
}
|
||||
deviations = {
|
||||
key: pstdev(seed_metrics[key] for seed_metrics in per_seed)
|
||||
for key in (*METRIC_NAMES, "fall_rate")
|
||||
}
|
||||
result = {
|
||||
"protocolVersion": 1,
|
||||
"taskId": task_id,
|
||||
"checkpoint": checkpoint.name,
|
||||
"checkpointSha256": _sha256(checkpoint),
|
||||
"seeds": list(cfg.seeds),
|
||||
"numEnvs": cfg.num_envs,
|
||||
"stepsPerSeed": cfg.steps_per_seed,
|
||||
"scenarios": [list(value) for value in SCENARIOS],
|
||||
"metrics": metrics,
|
||||
"metricStd": deviations,
|
||||
"seedMetrics": [
|
||||
{"seed": seed, "metrics": values}
|
||||
for seed, values in zip(cfg.seeds, per_seed, strict=True)
|
||||
],
|
||||
}
|
||||
output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
writer = SummaryWriter(log_dir=str(output.parent / "evaluation-events"))
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Actual checkpoint inference, capturing first-terminal physics before mjlab resets."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from statistics import fmean
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Also executable as a fresh-interpreter seed worker (never fork a CUDA context).
|
||||
for source in (Path(__file__).resolve().parents[1], Path(__file__).resolve().parents[2]):
|
||||
if str(source) not in sys.path:
|
||||
sys.path.insert(0, str(source))
|
||||
|
||||
import torch
|
||||
from mjlab.envs import ManagerBasedRlEnv
|
||||
from mjlab.rl import MjlabOnPolicyRunner, RslRlVecEnvWrapper
|
||||
from mjlab.sensor import ContactMatch, ContactSensorCfg
|
||||
from mjlab.tasks.registry import load_env_cfg, load_rl_cfg, load_runner_cls
|
||||
from scripts.train import _load_reward_config, _load_task_config
|
||||
from src.tasks.obstacle_avoidance.env_cfg import apply_obstacle_configuration
|
||||
from task_config import OBSTACLE_TASK
|
||||
from tuning.obstacle_scoring import METRICS, STEPS, protocol, score_trajectory
|
||||
from tuning.schema import apply_reward_configuration
|
||||
|
||||
|
||||
class FirstEpisodeRecorder:
|
||||
"""The termination hook runs post-physics, before _reset_idx (including first terminal)."""
|
||||
|
||||
def __init__(self, env, layout):
|
||||
self.env = env
|
||||
self.layout = layout
|
||||
self.samples = [[] for _ in range(env.num_envs)]
|
||||
self.previous = torch.zeros((env.num_envs, 12), device=env.device)
|
||||
self.calls = 0
|
||||
self.compute = env.termination_manager.compute
|
||||
env.termination_manager.compute = self.capture
|
||||
|
||||
def capture(self):
|
||||
env = self.env
|
||||
terminal = self.compute()
|
||||
robot = env.scene["robot"].data
|
||||
# Like mjlab termination itself, derived pose is one physics substep old.
|
||||
position = robot.root_link_pos_w
|
||||
local_xy = position[:, :2] - env.scene.env_origins[:, :2]
|
||||
xy = local_xy + torch.tensor(self.layout["spawn"][:2], device=env.device)
|
||||
boxes = self.layout["boxes"][1:] # Never include the support floor.
|
||||
clearance = torch.full((env.num_envs,), 0.5, device=env.device)
|
||||
if boxes:
|
||||
centers = torch.tensor([b["pos"][:2] for b in boxes], device=env.device)
|
||||
sizes = torch.tensor([b["size"][:2] for b in boxes], device=env.device)
|
||||
clearance = (
|
||||
((xy[:, None, :] - centers).abs() - sizes).clamp(min=0).norm(dim=2).amin(dim=1)
|
||||
)
|
||||
clearance = (clearance - 0.3).clamp(min=0) # Conservative body footprint radius.
|
||||
forces = env.scene["evaluation_obstacles"].data.force_history
|
||||
collision = forces.norm(dim=-1).flatten(1).amax(dim=1) > 1.0
|
||||
else:
|
||||
collision = torch.zeros(env.num_envs, dtype=torch.bool, device=env.device)
|
||||
nonfoot = env.scene["nonfoot_ground_touch"].data.force_history
|
||||
collision |= nonfoot.norm(dim=-1).flatten(1).amax(dim=1) > 10.0
|
||||
fall = (robot.projected_gravity_b[:, 2] > -0.3420201433) | (position[:, 2] < 0.12)
|
||||
actions = env.action_manager.action
|
||||
delta = (actions - self.previous).square().mean(dim=1)
|
||||
self.previous.copy_(actions)
|
||||
rays = env.scene["forward_scan"].data.distances
|
||||
hits = (
|
||||
((rays >= 0) & (rays <= env.scene["forward_scan"].cfg.max_distance)).float().mean(dim=1)
|
||||
)
|
||||
distance = env.command_manager.get_term("twist").errors()[1]
|
||||
values = torch.stack((distance, clearance, delta, hits, collision, fall, terminal), dim=1)
|
||||
if not torch.isfinite(values).all():
|
||||
raise ValueError("Nonfinite rollout measurements")
|
||||
keys = ("distance", "clearance", "action_delta", "ray_hit", "collision", "fall", "terminal")
|
||||
for samples, row in zip(self.samples, values.cpu().tolist(), strict=True):
|
||||
if not samples or not samples[-1]["terminal"]:
|
||||
samples.append(dict(zip(keys, row, strict=True)))
|
||||
self.calls += 1
|
||||
return terminal
|
||||
|
||||
def metrics(self, horizon=STEPS):
|
||||
if self.calls != horizon:
|
||||
raise ValueError("Incomplete rollout horizon")
|
||||
metrics = [score_trajectory(samples, horizon) for samples in self.samples]
|
||||
return {key: fmean(item[key] for item in metrics) for key in METRICS}
|
||||
|
||||
|
||||
def configure_seed(task_id, cfg, scenario, reward):
|
||||
env_cfg = load_env_cfg(task_id, play=False)
|
||||
env_cfg.seed = scenario["seed"]
|
||||
env_cfg.scene.num_envs = cfg.num_envs
|
||||
# Keep the benchmark's declared pair fixed; training itself randomizes every reset.
|
||||
apply_obstacle_configuration(env_cfg, scenario["taskConfig"], randomize_navigation=False)
|
||||
apply_reward_configuration(env_cfg, reward, task_id)
|
||||
env_cfg.curriculum = {}
|
||||
env_cfg.observations["actor"].enable_corruption = False
|
||||
env_cfg.events.pop("push_robot", None)
|
||||
# Primary geom names are literal compiled static terrain geoms, not a regex.
|
||||
names = tuple(f"terrain_{i}" for i in range(1, len(scenario["terrain"]["boxes"])))
|
||||
if names:
|
||||
env_cfg.scene.sensors += (
|
||||
ContactSensorCfg(
|
||||
name="evaluation_obstacles",
|
||||
primary=ContactMatch(mode="geom", pattern=names),
|
||||
secondary=ContactMatch(mode="subtree", pattern="base_link", entity="robot"),
|
||||
fields=("force",),
|
||||
reduce="maxforce",
|
||||
history_length=env_cfg.decimation,
|
||||
),
|
||||
)
|
||||
return env_cfg
|
||||
|
||||
|
||||
def evaluate_seed(task_id, cfg, scenario, reward):
|
||||
torch.manual_seed(scenario["seed"])
|
||||
env_cfg = configure_seed(task_id, cfg, scenario, reward)
|
||||
agent_cfg = load_rl_cfg(task_id)
|
||||
env = ManagerBasedRlEnv(env_cfg, device=cfg.device)
|
||||
wrapped = RslRlVecEnvWrapper(env, clip_actions=agent_cfg.clip_actions)
|
||||
try:
|
||||
runner_cls = load_runner_cls(task_id) or MjlabOnPolicyRunner
|
||||
runner = runner_cls(wrapped, asdict(agent_cfg), log_dir=None, device=wrapped.device)
|
||||
# rsl_rl actor state_dict includes observation_normalizer buffers. Strict load
|
||||
# rejects incompatible architecture/statistics; no fixture or zero-action fallback.
|
||||
runner.load(
|
||||
str(Path(cfg.checkpoint).resolve(strict=True)),
|
||||
load_cfg={"actor": True},
|
||||
strict=True,
|
||||
map_location=str(wrapped.device),
|
||||
)
|
||||
policy = runner.get_inference_policy(device=str(wrapped.device))
|
||||
obs, _ = env.reset(seed=scenario["seed"])
|
||||
recorder = FirstEpisodeRecorder(env, scenario["terrain"])
|
||||
with torch.inference_mode():
|
||||
for _ in range(STEPS):
|
||||
actions = policy(obs)
|
||||
if not torch.isfinite(actions).all():
|
||||
raise ValueError("Nonfinite policy actions")
|
||||
obs, _, _, _ = wrapped.step(actions)
|
||||
return {
|
||||
"seed": scenario["seed"],
|
||||
"metrics": recorder.metrics(),
|
||||
"episodes": cfg.num_envs,
|
||||
"rolloutSteps": STEPS,
|
||||
}
|
||||
finally:
|
||||
wrapped.close()
|
||||
|
||||
|
||||
def run_obstacle_evaluation(task_id, cfg):
|
||||
from tuning.obstacle_scoring import SEEDS
|
||||
|
||||
if tuple(cfg.seeds) != SEEDS or cfg.steps_per_seed != STEPS:
|
||||
raise ValueError("Obstacle evaluation seeds/horizon are fixed")
|
||||
if not cfg.task_config or not cfg.reward_config:
|
||||
raise ValueError("Obstacle evaluation requires task and parameter configurations")
|
||||
raw = json.loads(Path(cfg.task_config).read_text())
|
||||
custom = _load_task_config(task_id, cfg.task_config, raw["seed"])
|
||||
reward = _load_reward_config(cfg.reward_config, None, OBSTACLE_TASK)
|
||||
fixed = protocol(custom, cfg.num_envs)
|
||||
seeds = evaluate_isolated_seeds(task_id, cfg, fixed, reward)
|
||||
return {
|
||||
"protocol": fixed,
|
||||
"seedMetrics": seeds,
|
||||
"metrics": {key: fmean(item["metrics"][key] for item in seeds) for key in METRICS},
|
||||
}
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def evaluate_isolated_seeds(task_id, cfg, fixed, reward):
|
||||
from tuning.obstacle_scoring import validate_evaluation
|
||||
|
||||
seeds = []
|
||||
# Inherit the evaluation parent's process group: manager cancellation kills
|
||||
# both parent and the active seed. subprocess.run kills/reaps on timeout too.
|
||||
with tempfile.TemporaryDirectory(prefix="go2-obstacle-eval-") as directory:
|
||||
root = Path(directory)
|
||||
checkpoint = root / "checkpoint.pt"
|
||||
source_hash = file_hash(cfg.checkpoint)
|
||||
shutil.copyfile(cfg.checkpoint, checkpoint)
|
||||
if file_hash(checkpoint) != source_hash:
|
||||
raise ValueError("Checkpoint changed during snapshot")
|
||||
for scenario in fixed["scenarios"]:
|
||||
request = root / f"request-{scenario['seed']}.json"
|
||||
output = root / f"result-{scenario['seed']}.json"
|
||||
request.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"taskId": task_id,
|
||||
"config": {**asdict(cfg), "checkpoint": str(checkpoint)},
|
||||
"scenario": scenario,
|
||||
"reward": reward,
|
||||
"checkpointSha256": source_hash,
|
||||
},
|
||||
allow_nan=False,
|
||||
)
|
||||
)
|
||||
subprocess.run(
|
||||
[sys.executable, str(Path(__file__).resolve()), str(request), str(output)],
|
||||
check=True,
|
||||
timeout=1200,
|
||||
shell=False,
|
||||
)
|
||||
result = json.loads(output.read_text())
|
||||
if (
|
||||
result.pop("checkpointSha256", None) != source_hash
|
||||
or file_hash(checkpoint) != source_hash
|
||||
):
|
||||
raise ValueError("Seed checkpoint identity mismatch")
|
||||
seeds.append(result)
|
||||
candidate = {
|
||||
"protocol": fixed,
|
||||
"seedMetrics": seeds,
|
||||
"metrics": {key: fmean(item["metrics"][key] for item in seeds) for key in METRICS},
|
||||
}
|
||||
validate_evaluation(candidate, fixed)
|
||||
return seeds
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import src.tasks # noqa: F401
|
||||
|
||||
request_path, output_path = map(Path, sys.argv[1:])
|
||||
request = json.loads(request_path.read_text())
|
||||
config = SimpleNamespace(**request["config"])
|
||||
if file_hash(config.checkpoint) != request["checkpointSha256"]:
|
||||
raise ValueError("Worker checkpoint identity mismatch")
|
||||
result = evaluate_seed(request["taskId"], config, request["scenario"], request["reward"])
|
||||
result["checkpointSha256"] = file_hash(config.checkpoint)
|
||||
output_path.write_text(json.dumps(result, allow_nan=False))
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Literal, cast
|
||||
from typing import Literal
|
||||
|
||||
# 训练器作为仓库内置子集直接从 scripts/ 启动,不要求额外执行 pip install -e。
|
||||
TRAINER_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -34,7 +34,7 @@ from mjlab.utils.gpu import select_gpus
|
||||
from mjlab.utils.os import dump_yaml, get_checkpoint_path
|
||||
from mjlab.utils.torch import configure_torch_backends
|
||||
from mjlab.utils.wrappers import VideoRecorder
|
||||
|
||||
from task_config import OBSTACLE_TASK, deployment_metadata, validate_task_config
|
||||
from tuning.schema import apply_reward_configuration, validate_configuration
|
||||
|
||||
|
||||
@@ -51,8 +51,14 @@ class TrainConfig:
|
||||
gpu_ids: list[int] | Literal["all"] | None = field(default_factory=lambda: [0])
|
||||
output_dir: str | None = None
|
||||
resume_checkpoint: str | None = None
|
||||
pretrained_checkpoint: str | None = None
|
||||
pretrained_upload_manifest: str | None = None
|
||||
pretrained_onnx: str | None = None
|
||||
pretrained_source_id: str | None = None
|
||||
pretrained_allowed_roots: list[str] = field(default_factory=list)
|
||||
reward_config: str | None = None
|
||||
reward_config_json: str | None = None
|
||||
task_config: str | None = None
|
||||
|
||||
@staticmethod
|
||||
def from_task(task_id: str) -> "TrainConfig":
|
||||
@@ -61,27 +67,96 @@ class TrainConfig:
|
||||
return TrainConfig(env=env_cfg, agent=agent_cfg)
|
||||
|
||||
|
||||
def _load_reward_config(path: str | None, inline: str | None) -> dict | None:
|
||||
def _load_reward_config(path: str | None, inline: str | None, task_id="Unitree-Go2-Flat") -> dict | None:
|
||||
if path is not None and inline is not None:
|
||||
raise ValueError("Use only one of reward_config and reward_config_json")
|
||||
if inline is not None:
|
||||
if len(inline.encode("utf-8")) > 64 * 1024:
|
||||
raise ValueError("Reward configuration is larger than 64 KiB")
|
||||
return validate_configuration(json.loads(inline))
|
||||
return validate_configuration(json.loads(inline), task_id)
|
||||
if path is None:
|
||||
return None
|
||||
source = Path(path).expanduser().resolve(strict=True)
|
||||
if source.stat().st_size > 64 * 1024:
|
||||
raise ValueError("Reward configuration is larger than 64 KiB")
|
||||
with source.open(encoding="utf-8") as stream:
|
||||
return validate_configuration(json.load(stream))
|
||||
return validate_configuration(json.load(stream), task_id)
|
||||
|
||||
|
||||
def _load_task_config(task_id: str, path: str | None, seed: int) -> dict | None:
|
||||
if path is None:
|
||||
return validate_task_config(task_id, {}, seed)
|
||||
source = Path(path).expanduser().resolve(strict=True)
|
||||
if source.stat().st_size > 128 * 1024:
|
||||
raise ValueError("Task configuration is larger than 128 KiB")
|
||||
payload = json.loads(source.read_text(encoding="utf-8"))
|
||||
allowed = {"terrainPreset", "terrainParams", "sensorCfg", "seed", "customTerrainBoxes"}
|
||||
if not isinstance(payload, dict) or payload.keys() - allowed:
|
||||
raise ValueError("Unknown task configuration fields")
|
||||
if (
|
||||
isinstance(payload.get("seed"), bool)
|
||||
or not isinstance(payload.get("seed"), int)
|
||||
or payload.get("seed") != seed
|
||||
):
|
||||
raise ValueError("Task configuration seed must equal the agent seed")
|
||||
if payload.get("sensorCfg") is None:
|
||||
payload.pop("sensorCfg", None)
|
||||
return validate_task_config(task_id, payload, seed)
|
||||
|
||||
|
||||
def _configure_task_and_rewards(task_id: str, cfg: TrainConfig):
|
||||
custom = _load_task_config(task_id, cfg.task_config, cfg.agent.seed)
|
||||
if custom is not None:
|
||||
from src.tasks.obstacle_avoidance.env_cfg import apply_obstacle_configuration
|
||||
from src.tasks.obstacle_avoidance.terrain import apply_terrain_configuration
|
||||
if task_id == OBSTACLE_TASK:
|
||||
apply_obstacle_configuration(cfg.env, custom)
|
||||
else:
|
||||
apply_terrain_configuration(cfg.env, custom)
|
||||
deployment = deployment_metadata(task_id, custom, cfg.agent.seed)
|
||||
reward_config = _load_reward_config(cfg.reward_config, cfg.reward_config_json, task_id)
|
||||
if reward_config is not None:
|
||||
apply_reward_configuration(cfg.env, reward_config, task_id)
|
||||
if task_id == OBSTACLE_TASK:
|
||||
deployment["navigation"]["speed"] = reward_config["params"]["target_velocity"]
|
||||
deployment["sensorCfg"]["avoidanceWeight"] = reward_config["weights"]["avoidance_weight"]
|
||||
|
||||
return deployment, reward_config
|
||||
|
||||
|
||||
def _load_pretrained(cfg: TrainConfig):
|
||||
if cfg.pretrained_checkpoint is None:
|
||||
if (cfg.pretrained_onnx is not None or cfg.pretrained_allowed_roots
|
||||
or cfg.pretrained_source_id or cfg.pretrained_upload_manifest):
|
||||
raise ValueError("Pretrained options require --pretrained-checkpoint (.pt), not ONNX alone")
|
||||
return None
|
||||
if cfg.resume_checkpoint is not None or cfg.agent.resume:
|
||||
raise ValueError("Pretrained warm-start and resume are mutually exclusive")
|
||||
from pretrained import read_pretrained_source
|
||||
|
||||
options = {"onnx_path": cfg.pretrained_onnx}
|
||||
if cfg.pretrained_upload_manifest is not None:
|
||||
if cfg.pretrained_onnx is not None:
|
||||
raise ValueError("Uploaded actor cannot use adjacent ONNX sidecars")
|
||||
from pretrained_upload import read_uploaded_source
|
||||
read_pretrained_source = read_uploaded_source
|
||||
options = {"manifest_path": cfg.pretrained_upload_manifest}
|
||||
source = read_pretrained_source(
|
||||
cfg.pretrained_checkpoint,
|
||||
allowed_roots=cfg.pretrained_allowed_roots,
|
||||
**options,
|
||||
target_env=asdict(cfg.env),
|
||||
target_agent=asdict(cfg.agent),
|
||||
)
|
||||
if cfg.pretrained_source_id is not None and source.manifest["source_id"] != cfg.pretrained_source_id:
|
||||
raise ValueError("基础策略SHA身份变化,拒绝初始化")
|
||||
return source
|
||||
|
||||
|
||||
def run_train(task_id: str, cfg: TrainConfig, log_dir: Path) -> None:
|
||||
reward_config = _load_reward_config(cfg.reward_config, cfg.reward_config_json)
|
||||
if reward_config is not None:
|
||||
apply_reward_configuration(cfg.env, reward_config)
|
||||
|
||||
deployment, reward_config = _configure_task_and_rewards(task_id, cfg)
|
||||
# Validate source identity/semantics before allocating a simulation or optimizer.
|
||||
pretrained = _load_pretrained(cfg)
|
||||
cuda_visible = os.environ.get("CUDA_VISIBLE_DEVICES", "")
|
||||
if cuda_visible == "":
|
||||
device = "cpu"
|
||||
@@ -135,6 +210,17 @@ def run_train(task_id: str, cfg: TrainConfig, log_dir: Path) -> None:
|
||||
cfg=cfg.env, device=device, render_mode="rgb_array" if cfg.video else None
|
||||
)
|
||||
|
||||
if pretrained is not None:
|
||||
from pretrained import validate_runtime_contract
|
||||
|
||||
try:
|
||||
validate_runtime_contract(env)
|
||||
except Exception:
|
||||
env.close()
|
||||
raise
|
||||
|
||||
# The ONNX runner exports this exact snapshot beside and inside policy.onnx.
|
||||
env.platform_deployment = deployment
|
||||
log_root_path = log_dir.parent # Go up from specific run dir to experiment dir.
|
||||
|
||||
resume_path: Path | None = None
|
||||
@@ -171,9 +257,30 @@ def run_train(task_id: str, cfg: TrainConfig, log_dir: Path) -> None:
|
||||
runner = runner_cls(env, agent_cfg, str(log_dir), device, **runner_kwargs)
|
||||
|
||||
runner.add_git_repo_to_log(__file__)
|
||||
if pretrained is not None:
|
||||
from pretrained import initialize_runner
|
||||
|
||||
initialization = initialize_runner(runner, pretrained)
|
||||
env.unwrapped.platform_initialization = initialization
|
||||
if rank == 0:
|
||||
(log_dir / "initialization.json").write_text(
|
||||
json.dumps(initialization, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(f"[INFO] Warm-start actor from source {initialization['source_id']}; fresh critic/optimizer, iteration=0")
|
||||
if resume_path is not None:
|
||||
print(f"[INFO]: Loading model checkpoint from: {resume_path}")
|
||||
runner.load(str(resume_path), map_location=device)
|
||||
# Promotion copies the origin manifest, never the original actor weights.
|
||||
origin_path = resume_path.parent / "initialization.json"
|
||||
if origin_path.is_file():
|
||||
if origin_path.stat().st_size > 64 * 1024:
|
||||
raise ValueError("Initialization provenance exceeds 64 KiB")
|
||||
origin = json.loads(origin_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(origin, dict) or origin.get("mode") != "pretrained-warm-start":
|
||||
raise ValueError("Invalid initialization provenance")
|
||||
env.unwrapped.platform_initialization = origin
|
||||
if rank == 0:
|
||||
(log_dir / "initialization.json").write_text(json.dumps(origin, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
if explicit_resume:
|
||||
# RSL-RL stores the last completed zero-based iteration and otherwise
|
||||
# repeats it after load. Explicit tuning promotion uses an absolute target.
|
||||
@@ -260,7 +367,7 @@ def main():
|
||||
# Parse first argument to choose the task.
|
||||
# Import tasks to populate the registry.
|
||||
import mjlab.tasks # noqa: F401
|
||||
import src.tasks
|
||||
import src.tasks # noqa: F401
|
||||
|
||||
all_tasks = list_tasks()
|
||||
chosen_task, remaining_args = tyro.cli(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Service-owned CPU validator; JSON stdin is never accepted directly from HTTP."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for root in (ROOT, ROOT.parent):
|
||||
sys.path.insert(0, str(root))
|
||||
|
||||
|
||||
def main():
|
||||
from pretrained import read_pretrained_source
|
||||
from task_config import OBSTACLE_TASK
|
||||
from src.tasks.velocity.config.go2.env_cfgs import unitree_go2_flat_env_cfg
|
||||
from src.tasks.velocity.config.go2.rl_cfg import unitree_go2_ppo_runner_cfg
|
||||
|
||||
payload = json.loads(sys.stdin.read(128 * 1024))
|
||||
task_id = payload["taskId"]
|
||||
if task_id == "Unitree-Go2-Flat":
|
||||
cfg = unitree_go2_flat_env_cfg()
|
||||
elif task_id == OBSTACLE_TASK:
|
||||
from src.tasks.obstacle_avoidance.env_cfg import unitree_go2_obstacle_env_cfg, apply_obstacle_configuration
|
||||
cfg = unitree_go2_obstacle_env_cfg()
|
||||
if payload.get("taskConfig") is not None:
|
||||
apply_obstacle_configuration(cfg, payload["taskConfig"])
|
||||
else:
|
||||
raise ValueError("基础策略不兼容该任务;支持Flat47与Obstacle81/97,不支持Rough")
|
||||
directory = Path(payload["directory"])
|
||||
options = {}
|
||||
if payload.get("uploaded"):
|
||||
from pretrained_upload import read_uploaded_source
|
||||
read_pretrained_source = read_uploaded_source
|
||||
options["manifest_path"] = directory / "upload.json"
|
||||
source = read_pretrained_source(
|
||||
directory / payload["checkpoint"], allowed_roots=[directory], target_env=asdict(cfg),
|
||||
target_agent=asdict(unitree_go2_ppo_runner_cfg()), **options,
|
||||
)
|
||||
print(json.dumps(source.manifest))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as error:
|
||||
print(f"基础策略不兼容或缺少配套文件/依赖:{error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Resource-limited CPU single-file decoder; no network or neighboring model lookup."""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import resource
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Apply limits before importing tensor/protobuf runtimes. Thread env also set by parent.
|
||||
resource.setrlimit(resource.RLIMIT_CPU, (40, 40))
|
||||
resource.setrlimit(resource.RLIMIT_AS, (8 * 1024**3, 8 * 1024**3))
|
||||
resource.setrlimit(resource.RLIMIT_FSIZE, (16 * 1024**2, 16 * 1024**2))
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))
|
||||
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for root in (ROOT, ROOT.parent):
|
||||
sys.path.insert(0, str(root))
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
payload = json.loads(sys.stdin.read(4096))
|
||||
with contextlib.redirect_stdout(sys.stderr):
|
||||
import torch
|
||||
from pretrained_upload import import_upload
|
||||
|
||||
torch.set_num_threads(1)
|
||||
manifest = import_upload(
|
||||
payload["path"], payload["format"], payload["template"], payload["directory"]
|
||||
)
|
||||
print(json.dumps(manifest))
|
||||
except Exception as error:
|
||||
# Do not echo untrusted pickle/tensor names or absolute local paths.
|
||||
from pretrained import PretrainedError
|
||||
|
||||
message = (
|
||||
str(error)
|
||||
if isinstance(error, PretrainedError)
|
||||
else "文件解析失败;请提供受支持的自包含Go2 legacy47 actor"
|
||||
)
|
||||
print(json.dumps({"error": message[:500]}))
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user