Skip to content

Commit a0a3195

Browse files
authored
Merge pull request #90 from ImogenBits/battle_cores
Add option to execute battles on specific cores
2 parents ac2b500 + cb45bd5 commit a0a3195

File tree

3 files changed

+45
-9
lines changed

3 files changed

+45
-9
lines changed

algobattle/battle.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class FightHandler:
8282
_solver: Solver
8383
_battle: "Battle"
8484
_ui: FightUiProxy
85+
_set_cpus: str | None = None
8586

8687
def _saved(self, fight: Fight) -> Fight:
8788
self._battle.fight_results.append(fight)
@@ -139,6 +140,7 @@ async def run(
139140
cpus=cpus_generator,
140141
battle_input=generator_battle_input,
141142
battle_output=generator_battle_output,
143+
set_cpus=self._set_cpus,
142144
ui=ui.generator,
143145
)
144146
ui.update("generator", gen_result.info)
@@ -153,6 +155,7 @@ async def run(
153155
cpus=cpus_solver,
154156
battle_input=solver_battle_input,
155157
battle_output=solver_battle_output,
158+
set_cpus=self._set_cpus,
156159
ui=ui.solver,
157160
)
158161
ui.update("solver", sol_result.info)

algobattle/docker_util.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class DockerConfig(BaseModel):
5151

5252
build_timeout: float | None = None
5353
safe_build: bool = False
54+
set_cpus: str | list[str] | None = None
5455
generator: RunParameters = RunParameters()
5556
solver: RunParameters = RunParameters()
5657
advanced_run_params: "AdvancedRunArgs | None" = None
@@ -199,6 +200,7 @@ async def run(
199200
timeout: float | None = None,
200201
memory: int | None = None,
201202
cpus: int = 1,
203+
set_cpus: str | None = None,
202204
ui: ProgramUiProxy | None = None,
203205
) -> float:
204206
"""Runs a docker image.
@@ -209,6 +211,8 @@ async def run(
209211
timeout: Timeout in seconds.
210212
memory: Memory limit in MB.
211213
cpus: Number of physical cpus the container can use.
214+
set_cpus: Which cpus to execute the container on. Either a comma separated list or a hyphen-separated range.
215+
A value of `None` means the container can use any core (but still only `cpus` many of them).
212216
ui: Interface to update the ui with new data about the executing program.
213217
214218
Raises:
@@ -222,8 +226,8 @@ async def run(
222226
"""
223227
name = f"algobattle_{uuid1().hex[:8]}"
224228
if memory is not None:
225-
memory = int(memory * 1000000)
226-
cpus = int(cpus * 1000000000)
229+
memory = memory * 1_000_000
230+
cpus = cpus * 1_000_000_000
227231

228232
mounts = []
229233
if input_dir is not None:
@@ -243,6 +247,7 @@ async def run(
243247
nano_cpus=cpus,
244248
detach=True,
245249
mounts=mounts,
250+
cpuset_cpus=set_cpus,
246251
**self.run_kwargs,
247252
),
248253
)
@@ -410,6 +415,7 @@ async def _run(
410415
cpus: int = ...,
411416
battle_input: Encodable | None = None,
412417
battle_output: type[Encodable] | None = None,
418+
set_cpus: str | None = None,
413419
ui: ProgramUiProxy | None = None,
414420
) -> GeneratorResult | SolverResult:
415421
"""Execute the program, processing input and output data."""
@@ -450,7 +456,9 @@ async def _run(
450456
)
451457

452458
try:
453-
runtime = await self.image.run(input, output, timeout=timeout, memory=space, cpus=cpus, ui=ui)
459+
runtime = await self.image.run(
460+
input, output, timeout=timeout, memory=space, cpus=cpus, ui=ui, set_cpus=set_cpus
461+
)
454462
except ExecutionError as e:
455463
return result_class(
456464
ProgramRunInfo(
@@ -566,6 +574,7 @@ async def run(
566574
cpus: int = ...,
567575
battle_input: Encodable | None = None,
568576
battle_output: type[Encodable] | None = None,
577+
set_cpus: str | None = None,
569578
ui: ProgramUiProxy | None = None,
570579
) -> GeneratorResult:
571580
"""Executes the generator and parses its output into a problem instance.
@@ -577,6 +586,8 @@ async def run(
577586
cpus: Number of physical cpus the generator can use.
578587
battle_input: Additional data that will be given to the generator.
579588
battle_output: Class that will be used to parse additional data the generator outputs.
589+
set_cpus: Which cpus to execute the container on. Either a comma separated list or a hyphen-separated range.
590+
A value of `None` means the container can use any core (but still only `cpus` many of them).
580591
ui: Interface the program execution uses to update the ui.
581592
582593
Returns:
@@ -592,6 +603,7 @@ async def run(
592603
cpus=cpus,
593604
battle_input=battle_input,
594605
battle_output=battle_output,
606+
set_cpus=set_cpus,
595607
ui=ui,
596608
),
597609
)
@@ -632,6 +644,7 @@ async def run(
632644
cpus: int = ...,
633645
battle_input: Encodable | None = None,
634646
battle_output: type[Encodable] | None = None,
647+
set_cpus: str | None = None,
635648
ui: ProgramUiProxy | None = None,
636649
) -> SolverResult:
637650
"""Executes the solver on the given problem instance and parses its output into a problem solution.
@@ -643,6 +656,8 @@ async def run(
643656
cpus: Number of physical cpus the solver can use.
644657
battle_input: Additional data that will be given to the solver.
645658
battle_output: Class that will be used to parse additional data the solver outputs.
659+
set_cpus: Which cpus to execute the container on. Either a comma separated list or a hyphen-separated range.
660+
A value of `None` means the container can use any core (but still only `cpus` many of them).
646661
ui: Interface the program execution uses to update the ui.
647662
648663
Returns:
@@ -658,6 +673,7 @@ async def run(
658673
cpus=cpus,
659674
battle_input=battle_input,
660675
battle_output=battle_output,
676+
set_cpus=set_cpus,
661677
ui=ui,
662678
),
663679
)
@@ -710,7 +726,6 @@ class _UlimitArgs(TypedDict):
710726
cpu_rt_period: int | None = None
711727
cpu_rt_runtime: int | None = None
712728
cpu_shares: int | None = None
713-
cpuset_cpus: str | None = None
714729
cpuset_mems: str | None = None
715730
device_cgroup_rules: list[str] | None = None
716731
device_read_bps: list[_DeviceRate] | None = None

algobattle/match.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from itertools import combinations
66
from pathlib import Path
77
import tomllib
8-
from typing import Mapping, Self, overload
8+
from typing import Mapping, Self, cast, overload
99

1010
from pydantic import validator, Field
1111
from anyio import create_task_group, CapacityLimiter, TASK_STATUS_IGNORED
@@ -49,6 +49,14 @@ def val_battle_configs(cls, vals):
4949
out[name] = battle_cls.BattleConfig.parse_obj(data)
5050
return out
5151

52+
@validator("docker")
53+
def val_set_cpus(cls, v: DockerConfig, values) -> DockerConfig:
54+
"""Validates that each battle that is being executed is assigned some cpu cores."""
55+
if isinstance(v.set_cpus, list) and values["parallel_battles"] > len(v.set_cpus):
56+
raise ValueError("Number of parallel battles exceeds the number of set_cpu specifier strings.")
57+
else:
58+
return v
59+
5260
@classmethod
5361
def from_file(cls, file: Path) -> Self:
5462
"""Parses a config object from a toml file."""
@@ -75,16 +83,20 @@ async def _run_battle(
7583
matchup: Matchup,
7684
config: Battle.BattleConfig,
7785
problem: type[Problem],
86+
cpus: list[str | None],
7887
ui: "Ui",
7988
limiter: CapacityLimiter,
8089
*,
8190
task_status: TaskStatus = TASK_STATUS_IGNORED,
8291
) -> None:
8392
async with limiter:
93+
set_cpus = cpus.pop()
8494
ui.start_battle(matchup)
8595
task_status.started()
8696
battle_ui = ui.get_battle_observer(matchup)
87-
handler = FightHandler(matchup.generator.generator, matchup.solver.solver, battle, battle_ui.fight_ui)
97+
handler = FightHandler(
98+
matchup.generator.generator, matchup.solver.solver, battle, battle_ui.fight_ui, set_cpus
99+
)
88100
try:
89101
await battle.run_battle(
90102
handler,
@@ -94,8 +106,8 @@ async def _run_battle(
94106
)
95107
except Exception as e:
96108
battle.run_exception = str_with_traceback(e)
97-
finally:
98-
ui.battle_completed(matchup)
109+
cpus.append(set_cpus)
110+
ui.battle_completed(matchup)
99111

100112
@classmethod
101113
async def run(
@@ -122,6 +134,7 @@ async def run(
122134
Image.run_kwargs = config.docker.advanced_run_params.to_docker_args()
123135
if config.docker.advanced_build_params is not None:
124136
Image.run_kwargs = config.docker.advanced_build_params.to_docker_args()
137+
125138
with TeamHandler.build(config.teams, problem, config.docker) as teams:
126139
result = cls(
127140
active_teams=[t.name for t in teams.active],
@@ -132,11 +145,16 @@ async def run(
132145
battle_config = config.battle[config.battle_type]
133146
limiter = CapacityLimiter(config.parallel_battles)
134147
current_default_thread_limiter().total_tokens = config.parallel_battles
148+
set_cpus = config.docker.set_cpus
149+
if isinstance(set_cpus, list):
150+
match_cpus = cast(list[str | None], set_cpus[: config.parallel_battles])
151+
else:
152+
match_cpus = [set_cpus] * config.parallel_battles
135153
async with create_task_group() as tg:
136154
for matchup in teams.matchups:
137155
battle = battle_cls()
138156
result.results[matchup.generator.name][matchup.solver.name] = battle
139-
await tg.start(result._run_battle, battle, matchup, battle_config, problem, ui, limiter)
157+
await tg.start(result._run_battle, battle, matchup, battle_config, problem, match_cpus, ui, limiter)
140158
return result
141159

142160
@overload

0 commit comments

Comments
 (0)