Skip to content

Commit 89b10ee

Browse files
committed
feat: gather task resource info and reserve
Signed-off-by: elisam0 <elisam@nvidia.com>
1 parent 07756be commit 89b10ee

2 files changed

Lines changed: 76 additions & 3 deletions

File tree

responses_api_agents/anyterminal_agent/app.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,9 @@ class AnyTerminalAgentConfig(BaseResponsesAPIAgentConfig):
273273
)
274274
tb_agent_timeout: int = 1800
275275
tb_eval_timeout: int = 300
276-
apptainer_memory_limit_mb: int = 32768
276+
apptainer_memory_limit_mb: int = 32768 # fallback container memory cap when a task omits memory_mb
277+
agent_overhead_mb: int = 2048 # extra container memory on top of the task's memory_mb for the
278+
# in-container agent harness
277279
concurrency: int = 256
278280

279281

@@ -497,6 +499,13 @@ def _apptainer_exec(
497499
env: str = "",
498500
workdir: Optional[str] = None,
499501
) -> str:
502+
# NOTE: we do NOT hard-enforce the task's cpu/memory envelope on the container. Apptainer
503+
# cgroup flags (--cpus/--memory) are unusable here — rootless cgroups don't work under
504+
# --fakeroot ("cannot use cgroups - rootless cgroups is not usable in fakeroot mode") — and
505+
# per-task `ulimit -v` is unsafe (it caps virtual memory, which crashes torch/CUDA tasks at
506+
# import). So the container keeps the generous global virtual-memory cap, and the task's
507+
# cpu/memory footprint is only *reserved* in the Ray scheduler (_ray_resource_opts) to avoid
508+
# host oversubscription — not enforced as a hard per-task limit.
500509
pwd_flag = f"--pwd {shlex.quote(workdir)} " if workdir else ""
501510
cmd = (
502511
f"apptainer exec --writable-tmpfs --fakeroot --cleanenv --pid --no-mount home,tmp,bind-paths "
@@ -619,8 +628,34 @@ async def responses(self, body: NeMoGymResponseCreateParamsNonStreaming = Body()
619628
print(f"[{params.task_name}] exception: see {tb_path}", file=sys.stderr)
620629
raise
621630

631+
@staticmethod
632+
def _ray_resource_opts(params: AnyTerminalInstanceConfig) -> dict:
633+
"""Reserve the container's cpu/memory footprint in the Ray scheduler so concurrent containers
634+
don't oversubscribe the host — the main cause of the compute-starved "productive-but-timed-out"
635+
runs. The launcher task mostly awaits the apptainer subprocess, so these reservations are a
636+
proxy for the container's real resource use. Since rootless cgroups can't hard-cap the
637+
container here, this scheduling reservation is our only resource-isolation lever. Memory
638+
reserves the task's memory_mb + agent_overhead_mb (the in-container Hermes harness)."""
639+
640+
def _f(key):
641+
v = params.problem_info.get(key)
642+
try:
643+
return float(v) if v is not None else None
644+
except (TypeError, ValueError):
645+
return None
646+
647+
cpus = _f("cpus")
648+
mem_mb = _f("memory_mb")
649+
opts: dict = {"num_cpus": cpus if (cpus and cpus > 0) else 1}
650+
if mem_mb and mem_mb > 0:
651+
opts["memory"] = (int(mem_mb) + params.agent_overhead_mb) * 1024 * 1024
652+
gpus = _f("gpus") or 0
653+
if gpus > 0:
654+
opts["num_gpus"] = gpus
655+
return opts
656+
622657
async def _inner_responses(self, params: AnyTerminalInstanceConfig) -> NeMoGymResponse:
623-
await _run_remote.remote(params.model_dump())
658+
await _run_remote.options(**self._ray_resource_opts(params)).remote(params.model_dump())
624659

625660
persisted = TerminalBenchMetrics.model_validate_json(params.metrics_fpath.read_text())
626661
mask_sample = bool(persisted.container_timed_out or persisted.agent_timed_out)

responses_api_agents/anyterminal_agent/prepare.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,12 @@ def _load_task_config(task_dir: Path) -> dict:
6363
with open(config_path, "rb") as f:
6464
config = tomllib.load(f)
6565

66-
docker_image = (config.get("environment") or {}).get("docker_image", "ubuntu:22.04")
66+
env = config.get("environment") or {}
67+
docker_image = env.get("docker_image", "ubuntu:22.04")
6768
problem_statement = instruction_path.read_text() if instruction_path.exists() else ""
6869
agent_timeout = (config.get("agent") or {}).get("timeout_sec", None)
6970
verifier_timeout = (config.get("verifier") or {}).get("timeout_sec", None)
71+
resources = _parse_env_resources(env)
7072

7173
# Parse last WORKDIR from Dockerfile (if present).
7274
workdir = None
@@ -82,6 +84,38 @@ def _load_task_config(task_dir: Path) -> dict:
8284
"agent_timeout_sec": agent_timeout,
8385
"verifier_timeout_sec": verifier_timeout,
8486
"workdir": workdir,
87+
**resources,
88+
}
89+
90+
91+
def _mem_to_mb(val) -> int | None:
92+
"""Normalize a memory/storage spec to MB. Accepts an int (already MB, e.g. ``memory_mb``)
93+
or a string with a unit (``"2G"``, ``"512M"``, ``"10G"``)."""
94+
if val is None:
95+
return None
96+
if isinstance(val, (int, float)):
97+
return int(val)
98+
s = str(val).strip().upper().rstrip("B")
99+
units = {"K": 1 / 1024, "M": 1.0, "G": 1024.0, "T": 1024.0 * 1024}
100+
if s and s[-1] in units:
101+
return int(float(s[:-1]) * units[s[-1]])
102+
return int(float(s)) # bare number → assume MB
103+
104+
105+
def _parse_env_resources(env: dict) -> dict:
106+
"""Pull cpu/memory/storage/gpu limits from a task.toml ``[environment]`` table, tolerating
107+
both schema v1.0 (``memory = "2G"``) and v1.1 (``memory_mb = 2048``)."""
108+
memory_mb = env.get("memory_mb")
109+
if memory_mb is None:
110+
memory_mb = _mem_to_mb(env.get("memory"))
111+
storage_mb = env.get("storage_mb")
112+
if storage_mb is None:
113+
storage_mb = _mem_to_mb(env.get("storage"))
114+
return {
115+
"cpus": env.get("cpus"),
116+
"memory_mb": memory_mb,
117+
"storage_mb": storage_mb,
118+
"gpus": env.get("gpus", 0),
85119
}
86120

87121

@@ -129,6 +163,10 @@ def _to_gym_row(task_dir: Path, task_cfg: dict) -> dict:
129163
if task_cfg.get("verifier_timeout_sec") is not None
130164
else None,
131165
"workdir": task_cfg.get("workdir"),
166+
"cpus": str(task_cfg["cpus"]) if task_cfg.get("cpus") is not None else None,
167+
"memory_mb": str(task_cfg["memory_mb"]) if task_cfg.get("memory_mb") is not None else None,
168+
"storage_mb": str(task_cfg["storage_mb"]) if task_cfg.get("storage_mb") is not None else None,
169+
"gpus": str(task_cfg["gpus"]) if task_cfg.get("gpus") is not None else None,
132170
},
133171
},
134172
}

0 commit comments

Comments
 (0)