Source code for unilab.base.env_factory
"""Picklable ``EnvFactory`` adapters bridging uni_rl's injected env contract
to the UniLab env registry.
uni_rl runners and collectors never construct environments themselves; the
caller injects an ``EnvFactory`` (see ``uni_rl.env_contract``). Because
collectors run in ``multiprocessing`` spawn subprocesses, the factory must be
picklable by reference — the adapters below are module-level functions bound
with ``functools.partial`` (never closures or lambdas).
"""
from __future__ import annotations
from collections.abc import Mapping
from functools import partial
from typing import Any, cast
from uni_rl.env_contract import EnvFactory, EnvProtocol
from unilab.base.process_device import bind_genesis_process_device
[docs]
def make_registry_env(
task_name: str,
sim_backend: str,
num_envs: int,
env_cfg_override: Mapping[str, Any] | None = None,
) -> EnvProtocol:
"""``EnvFactory`` implementation backed by the UniLab env registry.
Top-level on purpose: ``functools.partial`` bindings of this function stay
picklable for spawn-based collector subprocesses. ``ensure_registries``
runs here because a spawned subprocess is a fresh interpreter that never
executed the parent process's registry bootstrap.
"""
from unilab.base import registry
from unilab.base.registry import ensure_registries
ensure_registries()
# Genesis owns a process-wide session whose Quadrants runtime binds the
# first entry of CUDA_VISIBLE_DEVICES. Off-policy/APPO collectors are
# fresh spawn processes, so the parent-side binding cannot reach them;
# carry the explicit cold-path id in the opaque override and bind
# immediately before registry construction. Binding a non-zero id pins
# CUDA_VISIBLE_DEVICES for this process, so forward the post-pin
# in-process index downstream. Newer unisim-core versions repeat this
# check in GenesisBackend itself, making this compatibility guard
# idempotent.
if sim_backend == "genesis" and env_cfg_override is not None:
genesis_device_id = env_cfg_override.get("genesis_device_id")
if genesis_device_id is not None:
if (
isinstance(genesis_device_id, bool)
or not isinstance(genesis_device_id, int)
or genesis_device_id < 0
):
raise ValueError(
"genesis_device_id must be a non-negative integer or None, "
f"got {genesis_device_id!r}"
)
bound = bind_genesis_process_device(f"cuda:{genesis_device_id}")
env_cfg_override = {
**env_cfg_override,
"genesis_device_id": int(bound.rsplit(":", 1)[1]),
}
# ABEnv satisfies EnvProtocol at runtime (reset/set_nan_guard live on
# NpEnv); the declared ABEnv type predates the uni_rl protocol.
return cast(
"EnvProtocol",
registry.make(
task_name,
sim_backend=sim_backend,
num_envs=num_envs,
env_cfg_override=dict(env_cfg_override) if env_cfg_override is not None else None,
),
)
[docs]
def registry_env_factory(task_name: str, sim_backend: str) -> EnvFactory:
"""Bind a registry task/backend pair into a picklable ``EnvFactory``."""
return partial(make_registry_env, task_name, sim_backend)
__all__ = ["make_registry_env", "registry_env_factory"]