Skip to content

Commit e224fc2

Browse files
authored
Add hardened QR v2 problem (#152)
1 parent 84b8c58 commit e224fc2

6 files changed

Lines changed: 722 additions & 0 deletions

File tree

problems/linalg.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ problems:
1010
deadline: "2026-06-30"
1111
gpus:
1212
- B200
13+
- directory: linalg/qr_v2
14+
name: qr_v2
15+
deadline: "2026-06-30"
16+
gpus:
17+
- B200

problems/linalg/qr_v2/eval.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import dataclasses
2+
import math
3+
import multiprocessing
4+
import os
5+
import re
6+
import sys
7+
import time
8+
from pathlib import Path
9+
from typing import Any, Optional
10+
11+
import torch
12+
13+
from reference import check_implementation, generate_input
14+
from utils import clear_l2_cache, set_seed
15+
16+
try:
17+
from task import TestSpec
18+
except ImportError:
19+
TestSpec = dict
20+
21+
22+
MAX_ITERATIONS_PER_BENCHMARK = 50
23+
BENCHMARK_INPUT_BYTES_TARGET = 256 * 1024 * 1024
24+
25+
26+
class PopcornOutput:
27+
def __init__(self, fd: int):
28+
self.file = os.fdopen(fd, "w")
29+
os.set_inheritable(fd, False)
30+
31+
def __enter__(self):
32+
return self
33+
34+
def __exit__(self, exc_type, exc_val, exc_tb):
35+
self.file.close()
36+
37+
def print(self, *args, **kwargs):
38+
print(*args, **kwargs, file=self.file, flush=True)
39+
40+
def log(self, key, value):
41+
self.print(f"{key}: {value}")
42+
43+
44+
@dataclasses.dataclass
45+
class TestCase:
46+
args: dict
47+
spec: str
48+
49+
50+
@dataclasses.dataclass
51+
class Stats:
52+
runs: int
53+
mean: float
54+
std: float
55+
err: float
56+
best: float
57+
worst: float
58+
59+
60+
def _combine(a: int, b: int) -> int:
61+
return int(a + (a + b) * (a + b + 1) // 2)
62+
63+
64+
def get_test_cases(file_name: str, seed: Optional[int]) -> list[TestCase]:
65+
try:
66+
content = Path(file_name).read_text()
67+
except Exception as exc:
68+
print(f"Could not open test file `{file_name}`: {exc}", file=sys.stderr)
69+
exit(113)
70+
71+
tests = []
72+
match = r"\s*([a-zA-Z]+):\s*([a-zA-Z]+|[+-]?[0-9]+)\s*"
73+
for line in content.splitlines():
74+
case = {}
75+
for part in line.split(";"):
76+
matched = re.match(match, part)
77+
if not re.fullmatch(match, part):
78+
print(f"invalid test case: '{line}': '{part}'", file=sys.stderr)
79+
exit(113)
80+
key = matched[1]
81+
val = matched[2]
82+
try:
83+
val = int(val)
84+
except ValueError:
85+
pass
86+
case[key] = val
87+
tests.append(TestCase(spec=line, args=case))
88+
89+
if seed is not None:
90+
for test in tests:
91+
if "seed" in test.args:
92+
test.args["seed"] = _combine(test.args["seed"], seed)
93+
return tests
94+
95+
96+
def calculate_stats(durations: list[float]) -> Stats:
97+
runs = len(durations)
98+
total = sum(durations)
99+
avg = total / runs
100+
variance = sum((x - avg) ** 2 for x in durations)
101+
std = math.sqrt(variance / (runs - 1)) if runs > 1 else 0.0
102+
err = std / math.sqrt(runs) if runs > 0 else 0.0
103+
return Stats(
104+
runs=runs,
105+
mean=avg,
106+
std=std,
107+
err=err,
108+
best=float(min(durations)),
109+
worst=float(max(durations)),
110+
)
111+
112+
113+
def _clone_data(data):
114+
if isinstance(data, tuple):
115+
return tuple(_clone_data(x) for x in data)
116+
if isinstance(data, list):
117+
return [_clone_data(x) for x in data]
118+
if isinstance(data, dict):
119+
return {k: _clone_data(v) for k, v in data.items()}
120+
if isinstance(data, torch.Tensor):
121+
return data.clone()
122+
return data
123+
124+
125+
def _run_single_test(test: TestCase):
126+
from submission import custom_kernel
127+
128+
data = generate_input(**test.args)
129+
torch.cuda.synchronize()
130+
output = custom_kernel(_clone_data(data))
131+
torch.cuda.synchronize()
132+
return check_implementation(data, output)
133+
134+
135+
def run_single_test(pool: multiprocessing.Pool, test: TestCase):
136+
return pool.apply(_run_single_test, (test,))
137+
138+
139+
def run_testing(logger: PopcornOutput, pool: multiprocessing.Pool, tests: list[TestCase]):
140+
passed = True
141+
logger.log("test-count", len(tests))
142+
for idx, test in enumerate(tests):
143+
logger.log(f"test.{idx}.spec", test.spec)
144+
good, message = run_single_test(pool, test)
145+
if good:
146+
logger.log(f"test.{idx}.status", "pass")
147+
if message:
148+
logger.log(f"test.{idx}.message", message)
149+
else:
150+
logger.log(f"test.{idx}.status", "fail")
151+
logger.log(f"test.{idx}.error", message)
152+
passed = False
153+
logger.log("check", "pass" if passed else "fail")
154+
return 0 if passed else 112
155+
156+
157+
def _make_data_batch(test: TestCase, count: int):
158+
args = dict(test.args)
159+
data_list = []
160+
for _ in range(count):
161+
if "seed" in args:
162+
args["seed"] += 42
163+
data_list.append(generate_input(**args))
164+
return data_list
165+
166+
167+
def _benchmark_batch_count(test: TestCase) -> int:
168+
batch = int(test.args.get("batch", 1))
169+
n = int(test.args.get("n", 1))
170+
# Input storage is A. Keep the generated batch modest
171+
# because large QR cases are already batched inside a single input.
172+
bytes_per_input = (batch * n * n) * 4
173+
if bytes_per_input <= 0:
174+
return 1
175+
return max(1, min(MAX_ITERATIONS_PER_BENCHMARK, BENCHMARK_INPUT_BYTES_TARGET // bytes_per_input))
176+
177+
178+
def _run_single_benchmark(
179+
test: TestCase,
180+
recheck: bool,
181+
max_repeats: int,
182+
max_time_ns: float,
183+
) -> Stats | Any:
184+
from submission import custom_kernel
185+
186+
data_list = _make_data_batch(test, _benchmark_batch_count(test))
187+
check_copy = _clone_data(data_list)
188+
189+
outputs = [custom_kernel(_clone_data(data)) for data in data_list]
190+
for reference_data, output in zip(check_copy, outputs):
191+
good, message = check_implementation(reference_data, output)
192+
if not good:
193+
return message
194+
195+
durations = []
196+
bm_start_time = time.perf_counter_ns()
197+
for i in range(max_repeats):
198+
torch.cuda.synchronize()
199+
clear_l2_cache()
200+
start_event = torch.cuda.Event(enable_timing=True)
201+
end_event = torch.cuda.Event(enable_timing=True)
202+
start_event.record()
203+
outputs = [custom_kernel(data) for data in data_list]
204+
end_event.record()
205+
torch.cuda.synchronize()
206+
durations.append(start_event.elapsed_time(end_event) * 1e6 / len(data_list))
207+
208+
if recheck:
209+
for reference_data, output in zip(check_copy, outputs):
210+
good, message = check_implementation(reference_data, output)
211+
if not good:
212+
return message
213+
214+
total_bm_duration = time.perf_counter_ns() - bm_start_time
215+
if i > 1 and total_bm_duration > 1e8:
216+
stats = calculate_stats(durations)
217+
if (
218+
stats.err / stats.mean < 0.001
219+
or stats.mean * stats.runs > max_time_ns
220+
or total_bm_duration > 120e9
221+
):
222+
break
223+
224+
return calculate_stats(durations)
225+
226+
227+
def run_single_benchmark(
228+
pool: multiprocessing.Pool,
229+
test: TestCase,
230+
recheck: bool,
231+
max_repeats: int,
232+
max_time_ns: float,
233+
):
234+
return pool.apply(_run_single_benchmark, (test, recheck, max_repeats, max_time_ns))
235+
236+
237+
def run_benchmarking(logger: PopcornOutput, pool: multiprocessing.Pool, tests: list[TestCase]):
238+
run_single_benchmark(pool, tests[0], False, 200, 10e7)
239+
240+
passed = True
241+
logger.log("benchmark-count", len(tests))
242+
for idx, test in enumerate(tests):
243+
logger.log(f"benchmark.{idx}.spec", test.spec)
244+
# recheck=True: re-validate the output of every timed iteration, not just
245+
# the pre-timing warmup. Without this, the timed loop (which for the
246+
# low-`count` shapes reuses one input object across all repeats) never
247+
# re-checks its outputs, so a kernel that diverges only inside the timed
248+
# region -- e.g. one that caches and replays an output keyed on the
249+
# reused input -- is scored as fast without ever being caught locally.
250+
# `leaderboard` mode already rechecks; this brings `benchmark` mode in
251+
# line so a wrong timed output fails here too.
252+
result = run_single_benchmark(pool, test, True, 200, 10e9)
253+
if isinstance(result, Stats):
254+
for field in dataclasses.fields(Stats):
255+
logger.log(f"benchmark.{idx}.{field.name}", getattr(result, field.name))
256+
else:
257+
logger.log(f"benchmark.{idx}.status", "fail")
258+
logger.log(f"benchmark.{idx}.error", result)
259+
passed = False
260+
logger.log("check", "pass" if passed else "fail")
261+
return 0 if passed else 112
262+
263+
264+
def main():
265+
fd = os.getenv("POPCORN_FD")
266+
if not fd:
267+
return 111
268+
if len(sys.argv) < 3:
269+
return 2
270+
271+
mode = sys.argv[1]
272+
seed = os.getenv("POPCORN_SEED")
273+
os.unsetenv("POPCORN_SEED")
274+
seed = int(seed) if seed else None
275+
set_seed(seed or 42)
276+
tests = get_test_cases(sys.argv[2], seed)
277+
278+
with PopcornOutput(int(fd)) as logger:
279+
mp_context = multiprocessing.get_context("spawn")
280+
with mp_context.Pool(1) as pool:
281+
if mode == "test":
282+
return run_testing(logger, pool, tests)
283+
if mode == "benchmark":
284+
return run_benchmarking(logger, pool, tests)
285+
if mode == "leaderboard":
286+
for test in tests:
287+
run_single_benchmark(pool, test, False, 1000, 5e8)
288+
logger.log("benchmark-count", len(tests))
289+
passed = True
290+
for idx, test in enumerate(tests):
291+
logger.log(f"benchmark.{idx}.spec", test.spec)
292+
result = run_single_benchmark(pool, test, True, 1000, 30e9)
293+
if isinstance(result, Stats):
294+
for field in dataclasses.fields(Stats):
295+
logger.log(f"benchmark.{idx}.{field.name}", getattr(result, field.name))
296+
else:
297+
logger.log(f"benchmark.{idx}.status", "fail")
298+
logger.log(f"benchmark.{idx}.error", str(result))
299+
passed = False
300+
break
301+
logger.log("check", "pass" if passed else "fail")
302+
return 0 if passed else 112
303+
if mode == "profile":
304+
logger.log("check", "fail")
305+
logger.log("error", "profile mode is not implemented for qr eval.py")
306+
return 2
307+
return 2
308+
309+
310+
if __name__ == "__main__":
311+
sys.exit(main())

0 commit comments

Comments
 (0)