Spaces:
Running
Running
Download kernel_env/server/worker.py from Shreyansh327/kernel-env: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/spaces/Shreyansh327/kernel-env/resolve/main/kernel_env/server/worker.py
- Command line
-
hf download hf://spaces/Shreyansh327/kernel-env/kernel_env/server/worker.py
-
curl -L -o worker.py https://huggingface.co/spaces/Shreyansh327/kernel-env/resolve/main/kernel_env/server/worker.py
14.9 kB
| """Evaluate one candidate kernel in an isolated process (GPU only). | |
| Usage: python -I worker.py request.json result.json | |
| The parent (evaluator.py) runs this with a timeout and a single visible GPU; crashes, hangs and | |
| CUDA context corruption die with the process. Result is written to a file, not stdout, so | |
| candidate prints cannot forge it. Candidate source has already passed static_checks. | |
| GPU sharing: several workers may run on one GPU. Each holds a shared flock on the GPU's lock file | |
| while it compiles and checks correctness, and an exclusive one while benchmarking, so no other | |
| eval's kernels run on that GPU during a timing. | |
| """ | |
| import contextlib | |
| import fcntl | |
| import json | |
| import math | |
| import sys | |
| import time | |
| import traceback | |
| PHASES: dict = {} # seconds per phase, returned with the result: where an eval's time went | |
| SOFTMAX = "level1/23_Softmax" | |
| FROBENIUS = "level1/37_FrobeniusNorm_" | |
| NORMALIZED_TASKS = {SOFTMAX, FROBENIUS} | |
| ACTIVATION_TASKS = {'level1/19_ReLU', 'level1/20_LeakyReLU', 'level1/31_ELU', | |
| 'level1/29_Softplus', 'level1/88_MinGPTNewGelu'} | |
| class HostOperationError(RuntimeError): | |
| pass | |
| def kernel_only_mode(device_type="cuda"): | |
| """Catch tensor operators that AST checks cannot distinguish from scalar/shape arithmetic. | |
| This checks PyTorch dispatch inside forward, not arbitrary native code or graphs built in __init__. | |
| Allocation, layout and device-local copies stay allowed. CPU calibration math in Triton's | |
| autotuner stays outside the guarded device; candidate inputs cannot be copied there for compute. | |
| """ | |
| from torch.utils._python_dispatch import TorchDispatchMode | |
| from torch.utils._pytree import tree_leaves | |
| import torch | |
| allowed = { | |
| "empty", "empty_like", "empty_strided", "new_empty", "new_empty_strided", | |
| "zeros", "zeros_like", "new_zeros", "ones", "ones_like", "full", "full_like", "new_full", | |
| "arange", "rand", "randn", "randint", "rand_like", "randn_like", | |
| "view", "_unsafe_view", "reshape", "as_strided", "slice", "select", "expand", | |
| "transpose", "t", "permute", "squeeze", "unsqueeze", "detach", "alias", | |
| "clone", "contiguous", "to", "_to_copy", "copy_", "zero_", "fill_", "lift_fresh", | |
| } | |
| class KernelOnlyMode(TorchDispatchMode): | |
| def __torch_dispatch__(self, func, types, args=(), kwargs=None): | |
| kwargs = kwargs or {} | |
| namespace, name = func._schema.name.split("::", 1) | |
| touches_device = any(isinstance(x, torch.Tensor) and x.device.type == device_type | |
| for x in tree_leaves((args, kwargs))) | |
| if namespace == "aten" and touches_device: | |
| off_device = (name == "_to_copy" and kwargs.get("device") is not None | |
| and torch.device(kwargs["device"]).type != device_type) | |
| off_device |= name == "copy_" and args[0].device.type != device_type | |
| if name not in allowed or off_device: | |
| raise HostOperationError(f"PyTorch operation '{func}' in forward; use a custom kernel") | |
| return func(*args, **kwargs) | |
| return KernelOnlyMode() | |
| def adversarial_inputs(torch, inputs, task_id: str, trial: int): | |
| """Keep task shapes; vary normalization inputs as well as RNG seeds. Never used for timing.""" | |
| if task_id in ACTIVATION_TASKS: | |
| if trial == 1: | |
| inputs[0].mul_(64).sub_(32) | |
| elif trial == 2: | |
| inputs[0].zero_() | |
| return inputs | |
| if task_id not in NORMALIZED_TASKS: | |
| return inputs | |
| x = inputs[0] | |
| if trial == 1: | |
| x.fill_(0 if task_id == SOFTMAX else 1) | |
| elif trial == 2: | |
| if task_id == SOFTMAX: | |
| x.zero_() | |
| rows = torch.arange(x.shape[0], device=x.device) | |
| x[rows, (rows * 104729) % x.shape[1]] = math.log(x.shape[1]) + 8 | |
| else: | |
| x.mul_(2).sub_(1) | |
| scales = torch.arange(1, x.shape[0] + 1, device=x.device) | |
| x.mul_(scales.reshape((-1,) + (1,) * (x.ndim - 1))) | |
| elif trial == 3 and task_id == FROBENIUS: | |
| x.zero_() | |
| x[(0,) * x.ndim] = 3 | |
| x[(-1,) * x.ndim] = -4 | |
| return inputs | |
| def numerical_error(torch, reference, actual, task_id: str, atol: float, rtol: float): | |
| """Return feedback on failure. Other tasks retain their configured tolerance (including TF32).""" | |
| if task_id in NORMALIZED_TASKS: | |
| if not torch.isfinite(reference).all() or not torch.isfinite(actual).all(): | |
| return "Normalization output must be finite" | |
| # Absolute 0.01 dwarfs these outputs. Scale its floor to the reference instead, | |
| # retaining the configured relative tolerance and a small allowance near zero. | |
| atol = min(atol, reference.abs().max().item() * 1e-5) | |
| if task_id == SOFTMAX: | |
| if (actual < 0).any() or not torch.allclose( | |
| actual.sum(dim=1, dtype=torch.float64), | |
| torch.ones(actual.shape[0], device=actual.device, dtype=torch.float64), | |
| atol=1e-4, rtol=rtol, | |
| ): | |
| return "Softmax probabilities must be nonnegative and each row must sum to 1" | |
| elif not math.isclose(torch.linalg.vector_norm(actual).item(), 1.0, abs_tol=1e-4, rel_tol=rtol): | |
| return "Frobenius normalization must have global L2 norm 1 (not one norm per slice)" | |
| if not torch.allclose(actual, reference, atol=atol, rtol=rtol, equal_nan=task_id not in NORMALIZED_TASKS): | |
| bad = (~torch.isclose(actual, reference, atol=atol, rtol=rtol, | |
| equal_nan=task_id not in NORMALIZED_TASKS)).sum().item() | |
| return f"{bad}/{actual.numel()} elements wrong (atol={atol:.3g}, rtol={rtol})" | |
| return None | |
| def _tb(limit: int = 2000) -> str: | |
| return traceback.format_exc()[-limit:] | |
| def _stage_for(e: BaseException) -> str: | |
| return "compile" if "CompilationError" in type(e).__name__ else "runtime" | |
| class Phase: | |
| def __init__(self, name): | |
| self.name = name | |
| def __enter__(self): | |
| self.t = time.monotonic() | |
| def __exit__(self, *exc): | |
| PHASES[self.name] = PHASES.get(self.name, 0.0) + time.monotonic() - self.t | |
| def evaluate(req: dict) -> dict: | |
| with Phase("import"): | |
| import torch | |
| from triton.testing import do_bench # bound before candidate import | |
| dev = torch.device("cuda") | |
| seeds = req["seeds"] | |
| lock = open(req["gpu_lock"], "a") if req.get("gpu_lock") else None | |
| def exclusive(): | |
| if lock is None: | |
| yield | |
| return | |
| # release before re-locking: an in-place shared->exclusive upgrade deadlocks when two workers | |
| # both hold the shared lock (seen under gVisor on Modal) | |
| with Phase("lock_wait"): | |
| fcntl.flock(lock, fcntl.LOCK_UN) | |
| fcntl.flock(lock, fcntl.LOCK_EX) # waits for other evals to leave the GPU | |
| try: | |
| yield | |
| finally: | |
| fcntl.flock(lock, fcntl.LOCK_UN) | |
| fcntl.flock(lock, base) | |
| # exclusive=True: retry of a run that hit OOM while sharing the GPU; hold the GPU alone throughout | |
| base = fcntl.LOCK_EX if req.get("exclusive") else fcntl.LOCK_SH | |
| if lock is not None: | |
| with Phase("lock_wait"): | |
| fcntl.flock(lock, base) | |
| def to_dev(x): | |
| return x.to(dev) if isinstance(x, torch.Tensor) else x | |
| def bench(model, inputs): | |
| with Phase("bench"), exclusive(), torch.no_grad(): | |
| return do_bench(lambda: model(*inputs), warmup=req["warmup_ms"], rep=req["rep_ms"], return_mode="median") | |
| ref_ns: dict = {} | |
| exec(compile(req["ref_src"], "reference.py", "exec"), ref_ns) | |
| with Phase("reference_init"), torch.device(dev): # KernelBench builds tensors on CPU; build them on the GPU | |
| init_inputs = [to_dev(x) for x in ref_ns["get_init_inputs"]()] | |
| torch.manual_seed(seeds[0]) | |
| ref = ref_ns["Model"](*init_inputs).to(dev).eval() | |
| def make_inputs(seed): | |
| with Phase("inputs"), torch.device(dev): | |
| torch.manual_seed(seed) | |
| return [to_dev(x) for x in ref_ns["get_inputs"]()] | |
| # Reference timing + roofline counts are cached per task by the evaluator (ref_cache) after the first run. | |
| # Otherwise time the reference before candidate code is imported, so import-time side effects can't slow it. | |
| out = dict(req["ref_cache"]) if req.get("ref_cache") else _measure_reference(torch, ref, make_inputs(seeds[0]), bench) | |
| out["gpu"] = torch.cuda.get_device_name() | |
| return _evaluate_candidate(req, torch, dev, ref, init_inputs, make_inputs, bench, out) | |
| def _measure_reference(torch, ref, inputs, bench) -> dict: | |
| out = {"t_ref_ms": bench(ref, inputs)} | |
| # roofline inputs: counted FLOPs (matmul/conv/attention) and compulsory bytes (inputs + params + outputs) | |
| try: | |
| from torch.utils.flop_counter import FlopCounterMode | |
| with torch.no_grad(), FlopCounterMode(display=False) as fc: | |
| ref_out = ref(*inputs) | |
| outs = ref_out if isinstance(ref_out, (tuple, list)) else [ref_out] | |
| read = [x for x in inputs if isinstance(x, torch.Tensor)] + list(ref.parameters()) + list(ref.buffers()) | |
| out["flops"] = fc.get_total_flops() | |
| out["bytes"] = sum(t.numel() * t.element_size() for t in read + [o for o in outs if isinstance(o, torch.Tensor)]) | |
| out["dtype"] = next((str(x.dtype) for x in read if x.is_floating_point()), None) | |
| except Exception: | |
| pass # roofline is optional; never fail an evaluation over it | |
| return out | |
| def _evaluate_candidate(req, torch, dev, ref, init_inputs, make_inputs, bench, out) -> dict: | |
| atol, rtol, seeds = req["atol"], req["rtol"], req["seeds"] | |
| cand_ns: dict = {"__name__": "candidate"} | |
| try: | |
| # compile from a real file: @triton.jit reads kernel source via inspect.getsource | |
| with Phase("import_candidate"), open(req["code_path"]) as f: | |
| exec(compile(f.read(), req["code_path"], "exec"), cand_ns) | |
| except BaseException: | |
| return {**out, "stage": "compile", "feedback": _tb()} | |
| try: | |
| torch.manual_seed(seeds[0]) # same seed => same parameter init as reference | |
| with torch.device(dev): | |
| new = cand_ns["ModelNew"](*init_inputs).to(dev).eval() | |
| except BaseException as e: | |
| return {**out, "stage": _stage_for(e), "feedback": _tb()} | |
| guard = kernel_only_mode(dev.type) | |
| def candidate(*inputs): | |
| with guard: | |
| return new(*inputs) | |
| max_err = 0.0 | |
| call_s = [] # wall time of each candidate call in the correctness trials (trial 0 includes JIT) | |
| for k, seed in enumerate(seeds): | |
| inputs = adversarial_inputs(torch, make_inputs(seed), req.get("task_id", ""), k) | |
| # the first call includes Triton JIT / CUDA compile time | |
| with Phase("correctness" if k else "first_call_incl_jit"), torch.no_grad(): | |
| ref_out = ref(*inputs) | |
| sums = [torch.sum(x, dtype=torch.float64).item() for x in inputs if isinstance(x, torch.Tensor)] | |
| try: | |
| t0 = time.monotonic() | |
| new_out = candidate(*inputs) | |
| torch.cuda.synchronize() | |
| call_s.append(time.monotonic() - t0) | |
| except HostOperationError as e: | |
| return {**out, "stage": "format", "correct": False, "feedback": str(e)} | |
| except BaseException as e: | |
| return {**out, "stage": _stage_for(e), "feedback": _tb()} | |
| refs = list(ref_out) if isinstance(ref_out, (tuple, list)) else [ref_out] | |
| news = list(new_out) if isinstance(new_out, (tuple, list)) else [new_out] | |
| if len(refs) != len(news): | |
| return {**out, "stage": "correctness", "correct": False, | |
| "feedback": f"Expected {len(refs)} outputs, got {len(news)}"} | |
| in_ptrs = {x.untyped_storage().data_ptr() for x in inputs if isinstance(x, torch.Tensor)} | |
| for i, (r, n) in enumerate(zip(refs, news)): | |
| if not isinstance(n, torch.Tensor) or n.shape != r.shape or n.dtype != r.dtype or n.device != r.device: | |
| got = (tuple(n.shape), n.dtype, n.device) if isinstance(n, torch.Tensor) else type(n).__name__ | |
| return {**out, "stage": "correctness", "correct": False, | |
| "feedback": f"Output {i}: expected {(tuple(r.shape), r.dtype, r.device)}, got {got}"} | |
| if n.untyped_storage().data_ptr() in in_ptrs: | |
| return {**out, "stage": "correctness", "correct": False, | |
| "feedback": f"Output {i} aliases an input tensor; allocate a new output"} | |
| err = (n.float() - r.float()).abs().nan_to_num(float("inf")).max().item() if n.numel() else 0.0 | |
| max_err = max(max_err, err) | |
| error = numerical_error(torch, r, n, req.get("task_id", ""), atol, rtol) | |
| if error: | |
| return {**out, "stage": "correctness", "correct": False, "max_abs_err": err, | |
| "feedback": f"Output {i} mismatch on trial {k}: {error}; max_abs_err={err:.3g}"} | |
| if sums != [torch.sum(x, dtype=torch.float64).item() for x in inputs if isinstance(x, torch.Tensor)]: | |
| return {**out, "stage": "correctness", "correct": False, | |
| "feedback": "Inputs were modified in place; kernels must not write to inputs"} | |
| del inputs, ref_out, new_out, refs, news | |
| t_ref = out["t_ref_ms"] | |
| # A kernel far slower than the reference would hold the GPU's exclusive lock for minutes in do_bench while | |
| # every other eval waits; its reward is ~0.1 regardless, so use the trial timing instead. | |
| trial_ms = 1000 * min(call_s[1:] or call_s) | |
| if trial_ms > max(req.get("slow_factor", 50) * t_ref, 1000.0): | |
| t_new, note = trial_ms, " (very slow: timed from the correctness trials, not benchmarked)" | |
| else: | |
| inputs = make_inputs(seeds[0]) | |
| try: | |
| t_new, note = bench(candidate, inputs), "" | |
| except HostOperationError as e: | |
| return {**out, "stage": "format", "correct": False, "feedback": str(e)} | |
| speedup = t_ref / t_new | |
| return {**out, "stage": "timed", "correct": True, "max_abs_err": max_err, "t_new_ms": t_new, | |
| "speedup": speedup, | |
| "feedback": f"Correct on {len(seeds)} trials. Reference {t_ref:.4f} ms, yours {t_new:.4f} ms{note}, " | |
| f"speedup {speedup:.2f}x on {out['gpu']}."} | |
| def main(req_path: str, out_path: str) -> None: | |
| with open(req_path) as f: | |
| req = json.load(f) | |
| try: | |
| res = evaluate(req) | |
| except BaseException: | |
| res = {"stage": "runtime", "feedback": _tb()} | |
| res["phases"] = {k: round(v, 3) for k, v in PHASES.items()} | |
| with open(out_path, "w") as f: | |
| json.dump(res, f) | |
| if __name__ == "__main__": | |
| main(sys.argv[1], sys.argv[2]) | |