Source code for unilab.base.cpu_runtime

"""Process-local CPU confinement for envs that own an explicit CPU block.

Multi-rank off-policy data-parallel runs partition host CPUs so each rank's
collector owns one contiguous block (``training.dp_collector_cpu_ids``, routed
into ``EnvCfg.cpu_ids``). The MuJoCo BatchEnvPool already pins its physics
workers to that block, but the collector's host-side compute did not follow:
Numba parallel kernels (``unisim.backend.body_state`` and the
motion-tracking kernels) size their pool from the host CPU count and leave
placement to the OS, so they drift across rank boundaries and compete with
sibling ranks' pinned physics workers.

``apply_env_cpu_runtime`` is the generic env-level counterpart, applied once on
the env-construction cold path — before managers, backend materialization, or
the first Numba parallel call exist:

- ``os.sched_setaffinity`` pins the calling thread to the block, and every
  already-running thread (e.g. BLAS pools spawned at ``import numpy``) is
  pinned individually via ``/proc/self/task``; threads spawned later
  (including Numba's lazily-launched pool) inherit the mask.
- ``numba.set_num_threads(len(cpu_ids))`` sizes Numba's pool to the block
  instead of the host, unless the operator pinned ``NUMBA_NUM_THREADS``
  (mirroring the motion-kernel runtime policy).

The function is backend-agnostic: ``EnvCfg.cpu_ids`` is the single source of
truth, so any backend whose env declares a block gets the same confinement.
"""

from __future__ import annotations

import os
import warnings
from collections.abc import Sequence

_PROC_TASK_DIR = "/proc/self/task"


def _confine_existing_threads(ids: set[int]) -> None:
    """Pin already-running threads; later threads inherit the caller's mask.

    Native pools spawned before env construction (OpenBLAS spawns its workers
    at ``import numpy``) would otherwise keep the host-wide mask and compete
    with sibling ranks' pinned CPUs. Threads may exit between listing and
    pinning; those races are ignored.
    """
    # Resolved at call time (not import) so tests can monkeypatch the seam;
    # ``getattr`` keeps this checkable on platforms where typeshed hides the
    # Linux-only symbol (mypy ``attr-defined`` on darwin).
    sched_setaffinity = getattr(os, "sched_setaffinity", None)
    if sched_setaffinity is None or not os.path.isdir(_PROC_TASK_DIR):
        return
    for entry in os.listdir(_PROC_TASK_DIR):
        try:
            sched_setaffinity(int(entry), ids)
        except OSError:
            continue


[docs] def apply_env_cpu_runtime(cpu_ids: Sequence[int] | None) -> None: """Confine this process's host-side compute to the env-owned CPU block. Cold path only: call from env construction, before the backend pool, managers, or any Numba parallel kernel exist. ``None`` (the single-rank default) is a no-op so the default path stays bit-identical. Structural validation (non-empty, unique, non-negative ints) is owned by ``EnvCfg.validate``; this function fails closed on CPU ids that are not available to the process. """ if cpu_ids is None: return ids = {int(cpu_id) for cpu_id in cpu_ids} sched_setaffinity = getattr(os, "sched_setaffinity", None) sched_getaffinity = getattr(os, "sched_getaffinity", None) if sched_setaffinity is not None and sched_getaffinity is not None: available = set(sched_getaffinity(0)) missing = sorted(ids - available) if missing: raise ValueError( f"EnvCfg.cpu_ids entries {missing} are not available to this process " f"(sched_getaffinity={sorted(available)})" ) sched_setaffinity(0, ids) _confine_existing_threads(ids) else: warnings.warn( "EnvCfg.cpu_ids process confinement requires os.sched_setaffinity " "(Linux); only the Numba thread cap is applied", stacklevel=2, ) if "NUMBA_NUM_THREADS" in os.environ: return from numba import set_num_threads try: set_num_threads(len(ids)) except ValueError as exc: # len(ids) <= NUMBA_NUM_THREADS holds whenever the pool default came # from this host's CPU count; warn instead of failing env construction. warnings.warn( f"EnvCfg.cpu_ids Numba thread cap to {len(ids)} rejected: {exc}", stacklevel=2, )