Skip to content

Commit ebbdc75

Browse files
authored
Merge pull request #369 from gkreitz/220_conservative_timelim
Update timelimit computation to new standard (also applying the change to legacy packages)
2 parents bbfcf33 + 56de805 commit ebbdc75

File tree

1 file changed

+60
-39
lines changed

1 file changed

+60
-39
lines changed

problemtools/verifyproblem.py

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import argparse
66
import concurrent.futures
77
from concurrent.futures import ThreadPoolExecutor
8+
import math
89
import threading
910
import queue
1011
import glob
@@ -108,7 +109,7 @@ class Context:
108109
def __init__(self, args: argparse.Namespace, executor: ThreadPoolExecutor | None) -> None:
109110
self.data_filter: Pattern[str] = args.data_filter
110111
self.submission_filter: Pattern[str] = args.submission_filter
111-
self.fixed_timelim: int | None = args.fixed_timelim
112+
self.fixed_timelim: float | None = args.fixed_timelim
112113
self.executor = executor
113114
self._background_work: list[concurrent.futures.Future[object]] = []
114115

@@ -343,7 +344,7 @@ def run_submission(self, sub, runner: Runner, context: Context) -> Result:
343344

344345
return (res, res_low, res_high)
345346

346-
def run_normal(self, sub, infile: Path, time_limit: int, feedback_dir: Path) -> SubmissionResult:
347+
def run_normal(self, sub, infile: Path, time_limit: float, feedback_dir: Path) -> SubmissionResult:
347348
"""
348349
Run a submission batch-style (non-interactive)
349350
"""
@@ -354,7 +355,7 @@ def run_normal(self, sub, infile: Path, time_limit: int, feedback_dir: Path) ->
354355
infile=str(infile),
355356
outfile=str(outfile),
356357
errfile=str(errfile),
357-
timelim=time_limit + 1,
358+
timelim=math.ceil(time_limit) + 1,
358359
memlim=self._problem.metadata.limits.memory,
359360
work_dir=sub.path,
360361
)
@@ -408,7 +409,7 @@ def run_submission_multipass(self, feedback_dir: Path, run_sub_fn) -> Submission
408409

409410
return SubmissionResult('JE', reason=f'Multipass validator did not give verdict in {validation_passes=} passes')
410411

411-
def run_submission_real(self, sub, context: Context, timelim: int, timelim_low: int, timelim_high: int) -> Result:
412+
def run_submission_real(self, sub, context: Context, timelim: float, timelim_low: float, timelim_high: float) -> Result:
412413
# This may be called off-main thread.
413414

414415
feedback_dir = Path(tempfile.mkdtemp(prefix=f'feedback-{self.counter}-', dir=self.problem.tmpdir))
@@ -989,7 +990,11 @@ def check(self, context: Context) -> bool:
989990

990991
if self._metadata.limits.time_limit is not None and not self._metadata.limits.time_limit.is_integer():
991992
self.warning(
992-
'Time limit configured to non-integer value. Problemtools does not yet support non-integer time limits, and will truncate'
993+
'Time limit configured to non-integer value. This can be fragile, and may not be supported by your CCS (Kattis does not).'
994+
)
995+
if not self._metadata.limits.time_resolution.is_integer():
996+
self.warning(
997+
'Time resolution is not an integer. This can be fragile, and may not be supported by your CCS (Kattis does not).'
993998
)
994999

9951000
return self._check_res
@@ -1476,7 +1481,7 @@ def validate_interactive(
14761481
self,
14771482
testcase: TestCase,
14781483
submission,
1479-
timelim: int,
1484+
timelim: float,
14801485
errorhandler: Submissions,
14811486
infile: str | None = None,
14821487
feedback_dir_path: str | None = None,
@@ -1489,7 +1494,7 @@ def validate_interactive(
14891494
errorhandler.error('Could not locate interactive runner')
14901495
return res
14911496
# file descriptor, wall time lim
1492-
initargs = ['1', str(2 * timelim)]
1497+
initargs = ['1', str(math.ceil(2 * timelim))]
14931498
validator_args = [infile if infile else testcase.infile, testcase.ansfile, '<feedbackdir>']
14941499
submission_args = submission.get_runcmd(memlim=self.problem.metadata.limits.memory)
14951500

@@ -1540,7 +1545,7 @@ def validate_interactive(
15401545
if sub_runtime > timelim:
15411546
sub_runtime = timelim
15421547
res = self._parse_validator_results(val, val_status, feedbackdir, testcase)
1543-
elif is_TLE(sub_status, True):
1548+
elif is_TLE(sub_status, True) or sub_runtime > timelim:
15441549
res = SubmissionResult('TLE')
15451550
elif is_RTE(sub_status):
15461551
res = SubmissionResult('RTE')
@@ -1622,7 +1627,7 @@ def validate(
16221627

16231628

16241629
class Runner:
1625-
def __init__(self, problem: Problem, sub, context: Context, timelim: int, timelim_low: int, timelim_high: int) -> None:
1630+
def __init__(self, problem: Problem, sub, context: Context, timelim: float, timelim_low: float, timelim_high: float) -> None:
16261631
self._problem = problem
16271632
self._sub = sub
16281633
self._context = context
@@ -1759,15 +1764,17 @@ def __str__(self) -> str:
17591764
return 'submissions'
17601765

17611766
def check_submission(
1762-
self, sub, context: Context, expected_verdict: Verdict, timelim: int, timelim_low: int, timelim_high: int
1767+
self, sub, context: Context, expected_verdict: Verdict, timelim: float, timelim_high: float
17631768
) -> SubmissionResult:
17641769
desc = f'{expected_verdict} submission {sub}'
17651770
partial = False
17661771
if expected_verdict == 'PAC':
1767-
# For partially accepted solutions, use the low timelim instead of the real one,
1768-
# to make sure we have margin in both directions.
17691772
expected_verdict = 'AC'
17701773
partial = True
1774+
# For partially accepted, we don't want to use them to lower bound the time limit, but we do want
1775+
# to warn if they're slow enough that they would have affected the time limit, had they been used
1776+
# to compute it.
1777+
timelim_low = timelim / self.problem.metadata.limits.time_multipliers.ac_to_time_limit
17711778
else:
17721779
timelim_low = timelim
17731780

@@ -1822,24 +1829,34 @@ def start_background_work(self, context: Context) -> None:
18221829
for sub in self._submissions[acr]:
18231830
context.submit_background_work(lambda s: s.compile(), sub)
18241831

1832+
def _compute_time_limit(self, fixed_limit: float | None, lower_bound_runtime: float | None) -> tuple[float, float]:
1833+
if fixed_limit is None and lower_bound_runtime is None:
1834+
# 5 minutes is our currently hard coded upper bound for what to allow when we don't know the time limit yet
1835+
return 300.0, 300.0
1836+
1837+
limits = self.problem.metadata.limits
1838+
if fixed_limit is not None:
1839+
timelim = fixed_limit
1840+
else:
1841+
assert lower_bound_runtime is not None, 'Assert to keep mypy happy'
1842+
exact_timelim = lower_bound_runtime * limits.time_multipliers.ac_to_time_limit
1843+
timelim = max(1, math.ceil(exact_timelim / limits.time_resolution)) * limits.time_resolution
1844+
1845+
return timelim, timelim * limits.time_multipliers.time_limit_to_tle
1846+
18251847
def check(self, context: Context) -> bool:
18261848
if self._check_res is not None:
18271849
return self._check_res
18281850
self._check_res = True
18291851

18301852
limits = self.problem.metadata.limits
1831-
time_multiplier = limits.time_multipliers.ac_to_time_limit
1832-
safety_margin = limits.time_multipliers.time_limit_to_tle
1853+
ac_to_time_limit = limits.time_multipliers.ac_to_time_limit
18331854

1834-
timelim_margin_lo = 300 # 5 minutes
1835-
timelim_margin = 300
1836-
timelim = 300
1855+
fixed_limit: float | None = context.fixed_timelim if context.fixed_timelim is not None else limits.time_limit
1856+
lower_bound_runtime: float | None = None # The runtime of the slowest submission used to lower bound the time limit.
18371857

1838-
if limits.time_limit is not None:
1839-
timelim = timelim_margin = int(limits.time_limit) # TODO: Support non-integer time limits
1840-
if context.fixed_timelim is not None:
1841-
timelim = context.fixed_timelim
1842-
timelim_margin = int(round(timelim * safety_margin))
1858+
if limits.time_limit is not None and context.fixed_timelim is not None:
1859+
self.warning('There is a fixed time limit in problem.yaml, and you provided one on command line. Using command line.')
18431860

18441861
for verdict in Submissions._VERDICTS:
18451862
acr = verdict[0]
@@ -1864,28 +1881,32 @@ def check(self, context: Context) -> bool:
18641881
self.error(f'Compile error for {acr} submission {sub}', additional_info=msg)
18651882
continue
18661883

1867-
res = self.check_submission(sub, context, acr, timelim, timelim_margin_lo, timelim_margin)
1884+
timelim, timelim_high = self._compute_time_limit(fixed_limit, lower_bound_runtime)
1885+
res = self.check_submission(sub, context, acr, timelim, timelim_high)
18681886
runtimes.append(res.runtime)
18691887

18701888
if acr == 'AC':
18711889
if len(runtimes) > 0:
1872-
max_runtime = max(runtimes)
1873-
exact_timelim = max_runtime * time_multiplier
1874-
max_runtime_str = f'{max_runtime:.3f}'
1875-
timelim = max(1, int(0.5 + exact_timelim)) # TODO: properly support 2023-07 time limit computation
1876-
timelim_margin_lo = max(1, min(int(0.5 + exact_timelim / safety_margin), timelim - 1))
1877-
timelim_margin = max(timelim + 1, int(0.5 + exact_timelim * safety_margin))
1878-
else:
1879-
max_runtime_str = None
1880-
if context.fixed_timelim is not None and context.fixed_timelim != timelim:
1881-
self.msg(
1882-
f' Solutions give timelim of {timelim} seconds, but will use provided fixed limit of {context.fixed_timelim} seconds instead'
1883-
)
1884-
timelim = context.fixed_timelim
1885-
timelim_margin = round(timelim * safety_margin)
1890+
lower_bound_runtime = max(runtimes)
1891+
1892+
# Helper function to format numbers with at most 3 decimals and dealing with None
1893+
def _f_n(number: float | None) -> str:
1894+
return f'{round(number, 3):g}' if number is not None else '-'
18861895

1896+
if fixed_limit is not None and lower_bound_runtime is not None:
1897+
if lower_bound_runtime * ac_to_time_limit > fixed_limit:
1898+
self.error(
1899+
f'Time limit fixed to {_f_n(fixed_limit)}, but slowest AC runs in {_f_n(lower_bound_runtime)} which is within a factor {_f_n(ac_to_time_limit)}.'
1900+
)
1901+
tl_from_subs, _ = self._compute_time_limit(None, lower_bound_runtime)
1902+
if not math.isclose(fixed_limit, tl_from_subs):
1903+
self.msg(
1904+
f' Solutions give timelim of {_f_n(tl_from_subs)} seconds, but will use provided fixed limit of {_f_n(fixed_limit)} seconds instead'
1905+
)
1906+
1907+
timelim, timelim_margin = self._compute_time_limit(fixed_limit, lower_bound_runtime)
18871908
self.msg(
1888-
f' Slowest AC runtime: {max_runtime_str}, setting timelim to {timelim} secs, safety margin to {timelim_margin} secs'
1909+
f' Slowest AC runtime: {_f_n(lower_bound_runtime)}, setting timelim to {_f_n(timelim)} secs, safety margin to {_f_n(timelim_margin)} secs'
18891910
)
18901911
self.problem._set_timelim(timelim)
18911912

@@ -2149,7 +2170,7 @@ def argparser() -> argparse.ArgumentParser:
21492170
parser.add_argument(
21502171
'-t',
21512172
'--fixed_timelim',
2152-
type=int,
2173+
type=float,
21532174
help='use this fixed time limit (useful in combination with -d and/or -s when all AC submissions might not be run on all data)',
21542175
)
21552176
parser.add_argument(

0 commit comments

Comments
 (0)