从 UniLab 1.4 迁移到 1.5¶
UniLab 1.5 将公共 reset 契约恢复为 IsaacLab/mjlab 风格的 env_ids 行索引形态(
issue #2460,隶属 roadmap
#2459)。v1.4 的定宽 bool mask
形态已直接移除、无兼容层
(issue #2474):向公共 reset
边界传 bool mask 会抛 TypeError。
本页说明签名变更、已移除的 mask 形态、mask-preserving term 如何回退为 env_ids
实现,以及下游任务如何删除 v1.4 时期的 temporal workaround。1.4 之前的实现即为恢复后
形态的参考——例如 git show v1.3.4:src/unilab/managers/event_manager.py。
变更清单¶
接口 |
v1.4 |
v1.5 |
|---|---|---|
|
|
|
|
|
|
有状态 class term 的 reset 钩子(reward/observation/termination/curriculum/metric) |
|
|
action term |
|
|
|
必须实现 class 边界 |
|
|
|
|
|
唯一的 reset 事件入口 |
已移除;使用 |
|
reset 事件不生效 |
恢复逐行触发过滤(1.3.x 语义) |
reset owner |
|
|
recorder 钩子 |
|
|
RL 环境契约 |
|
|
env_ids 是一维整数行索引张量;None(manager 边界还接受 slice)表示全部行。
恢复后的形态与 v1.3.x 公共契约一致,针对 1.3.x 编写的代码无需修改。
公共 env_ids 形态与 backend 事务所消费的定宽 bool mask 之间的内部转换只有唯一
一处:src/unilab/base/reset_selectors.py
(resolve_reset_env_ids、reset_env_ids_mask、resolve_reset_mask)。
backend 不受影响——见下方 FAQ。
已移除的 mask 形态¶
v1.5 —— 公共契约使用
env_ids。位置传参的定宽 bool 张量会抛TypeError。仍定义 v1.4 边界reset(env, reset_mask, **params)的 class 型 reset 事件项不再被检测:reset term 只以func(env, env_ids, **params)形式被调用。EventManager.dispatch_reset已移除;请使用apply(mode="reset", env_ids=...)。
关键字形式同样报错:v1.5 中 env.reset(reset_mask=mask) 会立即抛
TypeError。请将参数名改为 env_ids 并传行索引。
将 mask-preserving term 回退为 env_ids 实现¶
机械规则:
将 mask 参数替换为一维 int64 的
env_ids张量;values.masked_fill_(mask, x)改为values[env_ids] = x;torch.where(mask[:, None], new, old)改为values[env_ids] = new_rows;term 内部如需由 ids 得到 mask,使用
unilab.base.reset_selectors的reset_env_ids_mask(env_ids, num_envs=env.num_envs, device=env.device), 不要在别处手写转换;采样数量用
env_ids.numel(),而不是int(mask.sum())。
Event term(mode="reset")¶
v1.4 要求每个 reset 事件项实现 mask-preserving 的 class 边界。v1.5 以 mjlab 风格 调用 reset term,普通函数重新可用:
# v1.4 —— mask-preserving class term
class randomize_link_friction:
def __init__(self, cfg: EventTermCfg, env: ManagerBasedRlEnv):
self._friction = torch.ones(env.num_envs, device=env.device)
def reset(
self,
env: ManagerBasedRlEnv,
reset_mask: torch.Tensor,
friction_range: tuple[float, float],
) -> None:
rows = torch.nonzero(reset_mask, as_tuple=False).flatten()
sampled = env.rng.uniform(*friction_range, rows.numel())
self._friction[reset_mask] = sampled
# v1.5 —— term 以 func(env, env_ids, **params) 形式被调用;
# class term 将边界移到 __call__(普通函数亦可)
class randomize_link_friction:
def __init__(self, cfg: EventTermCfg, env: ManagerBasedRlEnv):
self._friction = torch.ones(env.num_envs, device=env.device)
def __call__(
self,
env: ManagerBasedRlEnv,
env_ids: torch.Tensor,
friction_range: tuple[float, float],
) -> None:
sampled = env.rng.uniform(*friction_range, env_ids.numel())
self._friction[env_ids] = sampled
有状态 class term 注意区分两个钩子:func(env, env_ids, **params) 是 reset 事件
本体,而 class 上单参数的 reset(self, env_ids) 仍是状态清理钩子(参见
src/unilab/envs/mdp/events.py 中 apply_body_impulse.reset)。双参数的
reset(env, reset_mask, **params) 形态已移除,不再被派发。写仿真状态的 event
term 仍通过环境持有的 packed reset transaction 提交,只有行选择器形态变化。
Reward / termination / curriculum / metric 的 class term¶
# v1.4
class episode_length_reward:
def reset(self, reset_mask: torch.Tensor) -> None:
self._steps.masked_fill_(reset_mask, 0)
# v1.5
class episode_length_reward:
def reset(self, env_ids: torch.Tensor) -> None:
self._steps[env_ids] = 0
Observation class term¶
钩子变化与 reward term 相同(参见
src/unilab/envs/mdp/observations.py 中 _NamedSensorObservation.reset)。
自定义 noise model 是例外:
src/unilab/managers/_noise/noise_model.py 中
NoiseModel.reset(reset_mask) 保持 mask 形态——ObservationManager 会替你完成
env_ids 到 mask 的转换,自定义 noise model 无需修改。
Action term¶
# v1.4
def reset(self, reset_mask: torch.Tensor) -> None:
self._raw_actions.masked_fill_(reset_mask.reshape(-1, 1), 0.0)
# v1.5(与 src/unilab/envs/mdp/actions/actions.py 一致)
def reset(self, env_ids: torch.Tensor | None = None) -> None:
if env_ids is None:
env_ids = slice(None)
self._raw_actions[env_ids] = 0.0
Command term¶
无需修改。公共 CommandTerm.reset(env_ids=None, ...) 在 manager 边界完成 mask
转换;受保护的 _resample_command(reset_mask) 子类钩子有意保持 v1.4 的定宽 mask
形态。
自定义 TorchEnv 子类¶
重命名行选择器,并在 backend 事务边界只做一次转换:
from unilab.base.reset_selectors import reset_env_ids_mask, resolve_reset_env_ids
def reset(self, env_ids: torch.Tensor | None = None):
ids = resolve_reset_env_ids(
env_ids, num_envs=self.num_envs, device=self.device, owner="MyEnv.reset"
)
...
backend_mask = reset_env_ids_mask(ids, num_envs=self.num_envs, device=self.device)
resolve_reset_env_ids 直接实施 v1.5 契约:bool mask 抛 TypeError、
越界索引同样被拒绝,因此 env 实现可以直接获得契约行为。
删除任务层 temporal workaround¶
v1.4 因缺少 mask reset 边界而移除了 manager 持有的 temporal 状态,任务只能自行堆叠
history。以 Wuji reorient 任务为例,它实现了任务级的 HistoryFeature(自带
CircularBuffer、通过 reset(reset_mask) 钩子做 reset_mask.nonzero() 解包,
另有 NoisyHistoryFeature 子类在每帧入栈前用 env.rng 手动加噪)。
随着 ObservationTermCfg 恢复 history_length 与 delay_* 原生配置(issue
#2462;语义与 1.3.4 一致——按
oldest → newest 时间顺序排布,reset 行用 reset 后首帧回填(backfill),
每个 term 的处理流水线为
compute → noise → clip → scale → delay → history,即噪声在入 history
之前施加),任务层的这些类可以删除:
# v1.4 任务 YAML(任务自持 history + 手写加噪)
joint:
func: wuji_unilab.tasks.reorient.observations.NoisyHistoryFeature
params:
kind: joint
history_length: 3
low: -0.06
high: 0.06
# v1.5 —— 普通 term + 原生 history_length 与 noise 配置
joint:
func: wuji_unilab.tasks.reorient.observations.feature
params: {kind: joint}
history_length: 3
noise:
_target_: unilab.managers._noise.UniformNoiseCfg
n_min: -0.06
n_max: 0.06
operation: add
展平后的宽度不变(history_length × term_dim,term-major,最旧在前——与
HistoryFeature 发布的布局一致),因此已训练策略的输入维度可以直接沿用。噪声只在
enable_corruption: true 的组上生效,与 workaround 的 actor-only 加噪一致。额外
声明 packed-read 元数据(tensor_state_entity / tensor_body_names,例如 Wuji 的
TipsFeature)的 class term 可以继续保持 class 形态:保留 __call__ 与元数据
property,删除 buffer、reset 钩子与手写加噪,并把
history_length/scale/noise 移到 term 配置上。
延迟观测改用 ObservationTermCfg 的原生 delay_min_lag / delay_max_lag /
delay_per_env / delay_hold_prob / delay_update_period / delay_per_env_phase
字段,不再使用任务层延迟队列;当组内所有 term 共享同一深度时,也可用组级
ObservationGroupCfg.history_length 统一覆盖。
FAQ¶
为什么 env.reset(reset_mask=mask) 直接抛 TypeError 而不是告警?
参数本身已从 reset_mask 更名为 env_ids,且 v1.4 的 mask 形态已不带
兼容窗口地移除:关键字形式与位置传参的 bool 张量在 v1.5 都会立即抛
TypeError。立即报错可以强迫用户现在就完成这一处改名,而不是在以后的
版本中爆发。
自定义 backend 或自定义 reset 事务需要修改吗? 不需要。
SimBackend.apply_reset 与 packed reset 事务仍消费定宽 bool mask;只有公共
用户边界恢复为 env_ids。src/unilab/base/reset_selectors.py 是唯一的转换点。
自定义 command term 或 noise model 需要修改吗? 不需要。command term 覆盖的
受保护钩子 _resample_command(reset_mask) 保持 v1.4 的 mask 形态;自定义
NoiseModel 子类保持 reset(reset_mask)。两个 manager 都在公共边界完成转换。
如何迁移 EventManager.dispatch_reset(mask, ...) 调用?
dispatch_reset 已在 v1.5 移除。替换为
event_manager.apply(mode="reset", env_ids=mask.nonzero(as_tuple=False).flatten(), global_env_step_count=...);
如果你手上已有行索引,直接传入即可。
v1.4 中 min_step_count_between_reset 的过滤去哪了? v1.4 的
dispatch_reset 忽略了该配置;v1.5 在
EventManager.apply(mode="reset", ...) 中恢复了 1.3.x 的逐行过滤,reset 事件
重新遵守两次触发之间的最小步数间隔。