From dfb2cffedaf09d9f8fbb250b30b6aa0145835c73 Mon Sep 17 00:00:00 2001 From: Kunal Mansukhani Date: Sun, 23 Nov 2025 00:05:29 -0800 Subject: [PATCH 1/3] refactor CD --- .github/workflows/deploy-challenges.yml | 59 ++++++++ challenges/core/challenge_base.py | 58 ++++++++ challenges/medium/36_radix_sort/challenge.py | 1 - scripts/update_challenges.py | 148 +++++++++++++++++++ 4 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy-challenges.yml create mode 100644 challenges/core/challenge_base.py create mode 100644 scripts/update_challenges.py diff --git a/.github/workflows/deploy-challenges.yml b/.github/workflows/deploy-challenges.yml new file mode 100644 index 0000000..35286a8 --- /dev/null +++ b/.github/workflows/deploy-challenges.yml @@ -0,0 +1,59 @@ +name: Deploy Challenges + +on: + push: + branches: + - main + paths: + - 'challenges/**' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + pip install requests + + - name: Get changed challenges + id: changed + run: | + git diff --name-only HEAD~1 HEAD | grep '^challenges/' > changed_files.txt || true + + if [ -s changed_files.txt ]; then + # Extract unique challenge directories (e.g., challenges/easy/1_vector_add -> challenges/easy/1_vector_add) + cat changed_files.txt | sed 's|\(challenges/[^/]*/[^/]*\)/.*|\1|' | sort -u > challenge_paths.txt + + if [ -s challenge_paths.txt ]; then + echo "paths<> $GITHUB_OUTPUT + cat challenge_paths.txt >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + fi + + - name: Deploy changed challenges + if: steps.changed.outputs.paths != '' + env: + SERVICE_URL: ${{ secrets.LEETGPU_SERVICE_URL }} + LEETGPU_API_KEY: ${{ secrets.LEETGPU_API_KEY }} + run: | + if [ -z "$LEETGPU_API_KEY" ]; then + echo "Error: LEETGPU_API_KEY secret is not set" + exit 1 + fi + + echo "${{ steps.changed.outputs.paths }}" | while read -r path; do + if [ -d "$path" ]; then + echo "Deploying $path" + python scripts/update_challenges.py "$path" + fi + done \ No newline at end of file diff --git a/challenges/core/challenge_base.py b/challenges/core/challenge_base.py new file mode 100644 index 0000000..8f1a652 --- /dev/null +++ b/challenges/core/challenge_base.py @@ -0,0 +1,58 @@ +from abc import ABC, abstractmethod +from typing import Any, List, Dict + +class ChallengeBase(ABC): + def __init__(self, name: str, atol: float, rtol: float, num_gpus: int, access_tier: str): + self.name = name + self.atol = atol + self.rtol = rtol + self.num_gpus = num_gpus + self.access_tier = access_tier + + @abstractmethod + def reference_impl(self, *args, **kwargs): + """ + Reference solution implementation. + """ + pass + + @abstractmethod + def get_solve_signature(self) -> Dict[str, Any]: + """ + Get the function signature for solution. + + Returns: + Dictionary with argtypes and restype for ctypes + """ + pass + + @abstractmethod + def generate_example_test(self) -> List[Dict[str, Any]]: + """ + Generate an example test case for this problem. + + Returns: + Dictionary with test case parameters + """ + pass + + @abstractmethod + def generate_functional_test(self) -> List[Dict[str, Any]]: + """ + Generate functional test cases for this problem. + + Returns: + List of test case dictionaries + """ + pass + + @abstractmethod + def generate_performance_test(self) -> List[Dict[str, Any]]: + """ + Generate a performance test case for this problem. + + Returns: + Dictionary with test case parameters + """ + pass + \ No newline at end of file diff --git a/challenges/medium/36_radix_sort/challenge.py b/challenges/medium/36_radix_sort/challenge.py index 3daefce..3a19cf6 100644 --- a/challenges/medium/36_radix_sort/challenge.py +++ b/challenges/medium/36_radix_sort/challenge.py @@ -1,7 +1,6 @@ import ctypes from typing import Any, List, Dict import torch -import numpy as np from core.challenge_base import ChallengeBase class Challenge(ChallengeBase): diff --git a/scripts/update_challenges.py b/scripts/update_challenges.py new file mode 100644 index 0000000..e64a974 --- /dev/null +++ b/scripts/update_challenges.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Deploy challenges to LeetGPU.com + +Environment variables: + SERVICE_URL - API service URL (default: http://localhost:8080) + LEETGPU_API_KEY - API key for authentication (required) +""" + +import argparse +import importlib.util +import logging +import os +import re +import sys +from pathlib import Path +from typing import Dict, Optional +import requests + +logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s") +logger = logging.getLogger(__name__) + +SERVICE_URL = os.getenv("SERVICE_URL", "http://localhost:8080") +LEETGPU_API_KEY = os.getenv("LEETGPU_API_KEY") + +GPUS = ["NVIDIA H100", "NVIDIA H200", "NVIDIA TESLA T4", "NVIDIA B200", "NVIDIA A100-80GB"] + +def extract_id(name: str) -> int: + match = re.match(r"^(\d+)_", name) + if not match: + raise ValueError(f"Directory name must start with number: {name}") + return int(match.group(1)) + +def get_difficulty(path: Path) -> str: + s = str(path).lower() + for d in ("easy", "medium", "hard"): + if d in s: + return d + return "easy" + +def get_language(filename: str) -> Optional[str]: + if filename == "starter.cu": + return "cuda" + if filename == "starter.mojo": + return "mojo" + if filename.startswith("starter.") and filename.endswith(".py"): + parts = filename.split(".") + if len(parts) == 3: + return parts[1] + return None + +def load_challenge(problem_dir: Path) -> Dict: + logger.info("Loading %s", problem_dir) + + problem_id = extract_id(problem_dir.name) + + spec_path = problem_dir / "challenge.html" + if not spec_path.exists(): + spec_path = problem_dir / "problem.html" + if not spec_path.exists(): + raise FileNotFoundError(f"No spec file in {problem_dir}") + + challenge_path = problem_dir / "challenge.py" + if not challenge_path.exists(): + raise FileNotFoundError(f"No challenge.py in {problem_dir}") + + challenges_dir = problem_dir.parent.parent + sys.path.insert(0, str(challenges_dir)) + + try: + spec = importlib.util.spec_from_file_location("challenge", challenge_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + challenge = module.Challenge() + title = challenge.name + access_tier = challenge.access_tier + finally: + sys.path.remove(str(challenges_dir)) + if "challenge" in sys.modules: + del sys.modules["challenge"] + + starter_code = [] + starter_dir = problem_dir / "starter" + if starter_dir.exists(): + for f in starter_dir.iterdir(): + if f.is_file() and (lang := get_language(f.name)): + starter_code.append({ + "language_name": lang, + "file_name": f.name, + "file_content": f.read_text() + }) + + return { + "id": problem_id, + "title": title, + "spec": spec_path.read_text(), + "challenge_code": challenge_path.read_text(), + "difficulty_level": get_difficulty(problem_dir), + "access_tier": access_tier, + "gpus": GPUS, # TODO: get from challenge.py or API + "starter_code": starter_code, + } + +def update_challenge(service_url: str, payload: Dict, api_key: str) -> bool: + url = f"{service_url.rstrip('/')}/api/v1/challenges" + headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} + try: + r = requests.post(url, json=payload, headers=headers, timeout=30) + r.raise_for_status() + logger.info("Updated challenge %s: %s", payload["id"], payload["title"]) + return True + except Exception as e: + logger.error("Failed challenge %s: %s", payload["id"], e) + return False + +def main(): + if not LEETGPU_API_KEY: + logger.error("LEETGPU_API_KEY environment variable is required") + return 1 + + parser = argparse.ArgumentParser() + parser.add_argument("path", type=Path, nargs="?") + args = parser.parse_args() + + if args.path: + dirs = [args.path] + else: + base = Path(__file__).parent.parent / "challenges" + dirs = [d for diff in base.iterdir() if diff.is_dir() + for d in diff.iterdir() if d.is_dir()] + + success = fail = 0 + for d in sorted(dirs): + try: + payload = load_challenge(d) + if update_challenge(SERVICE_URL, payload, LEETGPU_API_KEY): + success += 1 + else: + fail += 1 + except Exception as e: + logger.error("Failed %s: %s", d, e) + fail += 1 + + logger.info("Summary: %d succeeded, %d failed", success, fail) + return 0 if fail == 0 else 1 + +if __name__ == "__main__": + sys.exit(main()) From 0e2fcd33a73b212aa26da6213b8e085f1e11e30e Mon Sep 17 00:00:00 2001 From: Kunal Mansukhani Date: Sun, 7 Dec 2025 18:59:52 -0800 Subject: [PATCH 2/3] Add script to run challenge --- ...d_index.yml => check-duplicated-index.yml} | 0 scripts/generate_starter_code.py | 257 ------------------ scripts/requirements.txt | 2 + scripts/run_challenge.py | 124 +++++++++ scripts/update_challenges.py | 6 +- 5 files changed, 129 insertions(+), 260 deletions(-) rename .github/workflows/{check_duplicated_index.yml => check-duplicated-index.yml} (100%) delete mode 100644 scripts/generate_starter_code.py create mode 100644 scripts/requirements.txt create mode 100644 scripts/run_challenge.py diff --git a/.github/workflows/check_duplicated_index.yml b/.github/workflows/check-duplicated-index.yml similarity index 100% rename from .github/workflows/check_duplicated_index.yml rename to .github/workflows/check-duplicated-index.yml diff --git a/scripts/generate_starter_code.py b/scripts/generate_starter_code.py deleted file mode 100644 index 5be449e..0000000 --- a/scripts/generate_starter_code.py +++ /dev/null @@ -1,257 +0,0 @@ -import os -import sys -from importlib import util, machinery -import types -import ctypes -import urllib.request -import tempfile - -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -if PROJECT_ROOT not in sys.path: - sys.path.insert(0, PROJECT_ROOT) - -CONST_HINTS = { - "input" -} - -CTYPE_TO_CUDA = { - ctypes.c_int: "int", - ctypes.c_float: "float", - ctypes.c_double: "double", - ctypes.c_uint32: "unsigned int", - ctypes.c_int64: "long long", - ctypes.c_uint16: "__half", -} - -CTYPE_TO_MOJO = { - ctypes.c_int: "Int32", - ctypes.c_float: "Float32", - ctypes.c_double: "Float64", - ctypes.c_uint32: "UInt32", - ctypes.c_int64: "Int64", - ctypes.c_uint16: "Float16", -} - -CTYPE_TO_TORCH = { - ctypes.c_int: "int", - ctypes.c_float: "torch.float32", - ctypes.c_double: "torch.float64", - ctypes.c_uint32: "int", - ctypes.c_int64: "torch.int64", - ctypes.c_uint16: "torch.float16", -} - -CTYPE_TO_CUTE = { - ctypes.c_int: "cute.Int32", - ctypes.c_float: "cute.Float32", - ctypes.c_double: "cute.Float64", - ctypes.c_uint32: "cute.Uint32", - ctypes.c_int64: "cute.Int64", - ctypes.c_uint16: "cute.Float16", - ctypes.c_ulong: "cute.Uint64", -} - -def ctype_to_cuda(ctype, name) -> str: - if isinstance(ctype, type) and issubclass(ctype, ctypes._Pointer): - base_type = getattr(ctype, "_type_", None) - if base_type is None or base_type not in CTYPE_TO_CUDA: - raise ValueError( - f"Unsupported pointer base type: {base_type}. " - "Please extend CTYPE_TO_CUDA mapping." - ) - return f"{'const ' if name in CONST_HINTS else ''}{CTYPE_TO_CUDA[base_type]}*" - - if ctype not in CTYPE_TO_CUDA: - raise ValueError( - f"Unsupported scalar type: {ctype}. " - "Please extend CTYPE_TO_CUDA mapping." - ) - return CTYPE_TO_CUDA[ctype] - -def ctype_to_mojo(ctype) -> str: - if isinstance(ctype, type) and issubclass(ctype, ctypes._Pointer): - base_type = getattr(ctype, "_type_", None) - if base_type is None or base_type not in CTYPE_TO_MOJO: - raise ValueError( - f"Unsupported pointer base type: {base_type}. " - "Please extend CTYPE_TO_MOJO mapping." - ) - return f"UnsafePointer[{CTYPE_TO_MOJO[base_type]}]" - - if ctype not in CTYPE_TO_MOJO: - raise ValueError( - f"Unsupported scalar type: {ctype}. " - "Please extend CTYPE_TO_MOJO mapping." - ) - return CTYPE_TO_MOJO[ctype] - -def ctype_to_torch(ctype, name) -> str: - if isinstance(ctype, type) and issubclass(ctype, ctypes._Pointer): - return f"{name}: torch.Tensor" - - if ctype in (ctypes.c_int, ctypes.c_uint32, ctypes.c_int64): - return f"{name}: int" - if ctype in (ctypes.c_float, ctypes.c_double): - return f"{name}: float" - - raise ValueError( - f"Unsupported type {ctype} for PyTorch mapping. " - "Please extend CTYPE_TO_TORCH mapping." - ) - -def ctype_to_cute(ctype, name) -> str: - if isinstance(ctype, type) and issubclass(ctype, ctypes._Pointer): - return f"{name}: cute.Tensor" - - if ctype not in CTYPE_TO_CUTE: - raise ValueError( - f"Unsupported scalar type: {ctype}. " - "Please extend CTYPE_TO_CUTE mapping." - ) - return f"{name}: {CTYPE_TO_CUTE[ctype]}" - -def load_module(name: str, path: str): - spec = util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Could not load {name} from {path}") - - module = util.module_from_spec(spec) - spec.loader.exec_module(module) - sys.modules[name] = module - return module - -def load_challenge(challenge_dir: str): - base_url = "https://api.leetgpu.com/api/v1/core-files/challenge_base.py" - base_dst = os.path.join(tempfile.gettempdir(), "challenge_base.py") - urllib.request.urlretrieve(base_url, base_dst) - - sys.modules.setdefault("core", types.ModuleType("core")).__path__ = [] - - load_module("core.challenge_base", base_dst) - challenge = load_module("challenge", os.path.join(challenge_dir, "challenge.py")) - - return challenge.Challenge() - -def generate_starter_cuda(sig, starter_file): - arg_str = ", ".join(ctype_to_cuda(typ, name) + f" {name}" for name, typ in sig.items()) - include_half = "#include \n" if "__half" in arg_str else "" - - device_pointers = [name for name, typ in sig.items() if isinstance(typ, type) and issubclass(typ, ctypes._Pointer)] - comment = f"// {', '.join(device_pointers)} are device pointers" if device_pointers else "" - - code = f"""#include -{include_half} -{comment} -extern "C" void solve({arg_str}) {{ - -}}""" - with open(starter_file, "w") as f: - f.write(code) - -def generate_starter_mojo(sig, starter_file): - arg_str = ", ".join(f"{name}: {ctype_to_mojo(typ)}" for name, typ in sig.items()) - - device_pointers = [name for name, typ in sig.items() if isinstance(typ, type) and issubclass(typ, ctypes._Pointer)] - comment = f"# {', '.join(device_pointers)} are device pointers" if device_pointers else "" - - code = f"""from gpu.host import DeviceContext -from gpu.id import block_dim, block_idx, thread_idx -from memory import UnsafePointer -from math import ceildiv - -{comment} -@export -def solve({arg_str}): - pass""" - - with open(starter_file, "w") as f: - f.write(code) - -def generate_starter_pytorch(sig, starter_file): - arg_str = ", ".join(ctype_to_torch(typ, name) for name, typ in sig.items()) - - tensors = [name for name, typ in sig.items() if isinstance(typ, type) and issubclass(typ, ctypes._Pointer)] - comment = f"# {', '.join(tensors)} are tensors on the GPU" if tensors else "" - - code = f"""import torch - -{comment} -def solve({arg_str}): - pass -""" - with open(starter_file, "w") as f: - f.write(code) - -def generate_starter_triton(sig, starter_file): - def ctype_to_triton(ctype, name): - if isinstance(ctype, type) and issubclass(ctype, ctypes._Pointer): - return f"{name}: torch.Tensor" - if ctype in (ctypes.c_int, ctypes.c_uint32, ctypes.c_int64): - return f"{name}: int" - if ctype in (ctypes.c_float, ctypes.c_double): - return f"{name}: float" - raise ValueError(f"Unsupported type {ctype} for Triton mapping. Please extend ctype_to_triton mapping.") - - arg_str = ", ".join(ctype_to_triton(typ, name) for name, typ in sig.items()) - - tensors = [name for name, typ in sig.items() if isinstance(typ, type) and issubclass(typ, ctypes._Pointer)] - comment = f"# {', '.join(tensors)} are tensors on the GPU" if tensors else "" - - code = f"""import torch -import triton -import triton.language as tl - -{comment} -def solve({arg_str}): - pass -""" - with open(starter_file, "w") as f: - f.write(code) - -def generate_starter_cute(sig, starter_file): - arg_str = ", ".join(ctype_to_cute(typ, name) for name, typ in sig.items()) - - tensors = [name for name, typ in sig.items() if isinstance(typ, type) and issubclass(typ, ctypes._Pointer)] - comment = f"# {', '.join(tensors)} are tensors on the GPU" if tensors else "" - - code = f"""import cutlass -import cutlass.cute as cute - -{comment} -@cute.jit -def solve({arg_str}): - pass -""" - with open(starter_file, "w") as f: - f.write(code) - -def main(): - if len(sys.argv) != 2: - print("Usage: python scripts/generate_starter_code.py path/to/challenge_dir") - sys.exit(1) - - challenge_dir = sys.argv[1] - - if "easy" in (part.lower() for part in os.path.normpath(challenge_dir).split(os.sep)): - print("Starter code generation script should not be used for 'easy' challenges.") - sys.exit(1) - - starter_dir = os.path.join(challenge_dir, "starter") - - try: - os.makedirs(starter_dir, exist_ok=True) - except Exception as e: - print(f"Error creating starter directory: {e}") - sys.exit(1) - - challenge = load_challenge(challenge_dir) - sig = challenge.get_solve_signature() - - generate_starter_cuda(sig, os.path.join(starter_dir, "starter.cu")) - generate_starter_mojo(sig, os.path.join(starter_dir, "starter.mojo")) - generate_starter_pytorch(sig, os.path.join(starter_dir, "starter.pytorch.py")) - generate_starter_triton(sig, os.path.join(starter_dir, "starter.triton.py")) - generate_starter_cute(sig, os.path.join(starter_dir, "starter.cute.py")) - -if __name__ == "__main__": - main() diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..23e7b75 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,2 @@ +requests +websocket-client \ No newline at end of file diff --git a/scripts/run_challenge.py b/scripts/run_challenge.py new file mode 100644 index 0000000..e391059 --- /dev/null +++ b/scripts/run_challenge.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Development helper to upsert a challenge and submit a solution via /ws/submit. + +Usage: + python scripts/run_challenge.py /path/to/challenges/easy/1_vector_add + +Env vars: + SERVICE_URL - API base URL (default: localhost:8080) + LEETGPU_API_KEY - required, Bearer token +""" + +import argparse +import json +import logging +import sys +from pathlib import Path + +import websocket + +from update_challenges import ( + load_challenge, + update_challenge, + SERVICE_URL, + LEETGPU_API_KEY, +) + + +logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s") +logger = logging.getLogger(__name__) + + +def find_solution_file(challenge_dir: Path) -> tuple[str, str]: + solution_dir = challenge_dir / "solution" + if not solution_dir.is_dir(): + raise FileNotFoundError("No solution directory found. Add a solution/ folder with your file.") + candidates = sorted([p for p in solution_dir.iterdir() if p.is_file()]) + if not candidates: + raise FileNotFoundError("solution/ directory is empty.") + path = candidates[0] + return path.name, path.read_text() + + +def submit_solution(ws_url: str, api_key: str, challenge_id: int, file_name: str, content: str, + language: str, gpu: str, action: str, public: bool) -> bool: + + headers = [f"Authorization: Bearer {api_key}"] if api_key else [] + ws = websocket.create_connection(ws_url, header=headers, timeout=120) + try: + submission = { + "action": action, + "submission": { + "files": [{"name": file_name, "content": content}], + "language": language, + "gpu": gpu, + "mode": "accelerated", + "public": public, + "challenge_id": challenge_id, + }, + } + ws.send(json.dumps(submission)) + logger.info("Submitted %s to challenge %s", file_name, challenge_id) + + while True: + msg = ws.recv() + if not msg: + continue + data = json.loads(msg) + status = data.get("status") + output = data.get("output") + logger.info("Status: %s | Output: %s", status, output) + if status in {"success", "error", "timeout", "oom", "interrupted"}: + return status == "success" + finally: + ws.close() + + +def main() -> int: + if not LEETGPU_API_KEY: + logger.error("LEETGPU_API_KEY environment variable is required") + return 1 + + parser = argparse.ArgumentParser(description="Upsert a challenge and submit a solution via API.") + parser.add_argument("challenge_path", type=Path, help="Path to the challenge directory") + parser.add_argument("--language", default="cuda", help="Language (default: cuda)") + parser.add_argument("--gpu", default="NVIDIA TESLA T4", help="GPU name (default: NVIDIA TESLA T4)") + parser.add_argument("--action", default="run", choices=["run", "submit"], help="Action (run or submit)") + args = parser.parse_args() + + # Upsert challenge + try: + payload = load_challenge(args.challenge_path) + except Exception as e: + logger.error("Failed to load challenge: %s", e) + return 1 + + if not update_challenge(SERVICE_URL, payload, LEETGPU_API_KEY): + logger.error("Upsert failed") + return 1 + + # Submit solution + try: + file_name, content = find_solution_file(args.challenge_path) + except Exception as e: + logger.error("Failed to find solution file: %s", e) + return 1 + + ok = submit_solution( + ws_url=f"ws://{SERVICE_URL}/ws/submit", + api_key=LEETGPU_API_KEY, + challenge_id=payload["id"], + file_name=file_name, + content=content, + language=args.language, + gpu=args.gpu, + action=args.action, + public=False, + ) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/scripts/update_challenges.py b/scripts/update_challenges.py index e64a974..244557b 100644 --- a/scripts/update_challenges.py +++ b/scripts/update_challenges.py @@ -3,7 +3,7 @@ Deploy challenges to LeetGPU.com Environment variables: - SERVICE_URL - API service URL (default: http://localhost:8080) + SERVICE_URL - API service URL (default: localhost:8080) LEETGPU_API_KEY - API key for authentication (required) """ @@ -20,7 +20,7 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s") logger = logging.getLogger(__name__) -SERVICE_URL = os.getenv("SERVICE_URL", "http://localhost:8080") +SERVICE_URL = os.getenv("SERVICE_URL", "localhost:8080") LEETGPU_API_KEY = os.getenv("LEETGPU_API_KEY") GPUS = ["NVIDIA H100", "NVIDIA H200", "NVIDIA TESLA T4", "NVIDIA B200", "NVIDIA A100-80GB"] @@ -102,7 +102,7 @@ def load_challenge(problem_dir: Path) -> Dict: } def update_challenge(service_url: str, payload: Dict, api_key: str) -> bool: - url = f"{service_url.rstrip('/')}/api/v1/challenges" + url = f"http://{service_url.rstrip('/')}/api/v1/challenges" headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} try: r = requests.post(url, json=payload, headers=headers, timeout=30) From 0c0d007d6c688edefd3e4c07490c580bfcd76897 Mon Sep 17 00:00:00 2001 From: Kunal Mansukhani Date: Thu, 11 Dec 2025 02:19:38 -0800 Subject: [PATCH 3/3] fix lang --- scripts/run_challenge.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/run_challenge.py b/scripts/run_challenge.py index e391059..1d6f38c 100644 --- a/scripts/run_challenge.py +++ b/scripts/run_challenge.py @@ -30,15 +30,19 @@ logger = logging.getLogger(__name__) -def find_solution_file(challenge_dir: Path) -> tuple[str, str]: - solution_dir = challenge_dir / "solution" - if not solution_dir.is_dir(): - raise FileNotFoundError("No solution directory found. Add a solution/ folder with your file.") - candidates = sorted([p for p in solution_dir.iterdir() if p.is_file()]) - if not candidates: - raise FileNotFoundError("solution/ directory is empty.") - path = candidates[0] - return path.name, path.read_text() +def find_solution_file(challenge_dir: Path, language: str) -> tuple[str, str]: + language_to_extension = { + "cuda": "cu", + "mojo": "mojo", + "pytorch": "pytorch.py", + "cute": "cute.py", + "triton": "triton.py", + } + + solution_file = challenge_dir / "solution" / f"solution.{language_to_extension[language]}" + if not solution_file.exists(): + raise FileNotFoundError(f"No solution file found for {language}. Add a solution/{language}.py file.") + return solution_file.name, solution_file.read_text() def submit_solution(ws_url: str, api_key: str, challenge_id: int, file_name: str, content: str, @@ -100,7 +104,7 @@ def main() -> int: # Submit solution try: - file_name, content = find_solution_file(args.challenge_path) + file_name, content = find_solution_file(args.challenge_path, args.language) except Exception as e: logger.error("Failed to find solution file: %s", e) return 1