From d3a880e5c90dc3c73b9cb70e23d6f291438d4833 Mon Sep 17 00:00:00 2001 From: HenryHZY <168133331@qq.com> Date: Mon, 22 Jun 2026 12:04:12 +0800 Subject: [PATCH 1/4] add solution button --- README.md | 6 +-- START_WEB_UI.md | 48 +++++++++++++++++++ api/main.py | 15 +++++- api/parser.py | 60 +++++++++++++++++++++++ api/requirements.txt | 1 + web/package-lock.json | 108 ------------------------------------------ web/src/app/page.tsx | 72 ++++++++++++++++++++++++++++ 7 files changed, 198 insertions(+), 112 deletions(-) create mode 100644 START_WEB_UI.md diff --git a/README.md b/README.md index 2b2fd2e..c2504d3 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ For a modern, standalone coding experience with an integrated IDE and dual-pane ``` 2. **Start Frontend (Next.js):** ```bash - cd web - npm install - npm run dev +cd web +npm install +npm run dev ``` 3. Open **** in your browser. diff --git a/START_WEB_UI.md b/START_WEB_UI.md new file mode 100644 index 0000000..a6ebfc0 --- /dev/null +++ b/START_WEB_UI.md @@ -0,0 +1,48 @@ +# Start Standalone Web UI + +## First-Time Setup + +Run these from the project root: + +```bash +python -m pip install -r api/requirements.txt +cd web && npm install +``` + +## Start Backend + +Terminal 1, from the project root: + +```bash +python -m uvicorn api.main:app --port 8000 --reload +``` + +## Start Frontend + +Terminal 2, from the project root: + +```bash +cd web && WATCHPACK_POLLING=true NEXT_TELEMETRY_DISABLED=1 npm run dev -- --hostname 127.0.0.1 --port 3000 +``` + +Open: + +```text +http://127.0.0.1:3000/ +``` + +## Stop Servers + +Press `Ctrl+C` in both terminals, or run: + +```bash +lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill +lsof -tiTCP:8000 -sTCP:LISTEN | xargs -r kill +``` + +## Quick Checks + +```bash +curl -I http://127.0.0.1:3000 +curl -I http://127.0.0.1:8000/docs +``` diff --git a/api/main.py b/api/main.py index dfda7ea..7f2661d 100644 --- a/api/main.py +++ b/api/main.py @@ -9,7 +9,7 @@ from torch_judge.tasks import TASKS, get_task from torch_judge.web_engine import execute_code -from api.parser import get_all_templates +from api.parser import get_all_solutions, get_all_templates app = FastAPI(title="TorchCode UI Backend") @@ -24,6 +24,7 @@ # Load templates on startup TEMPLATES = get_all_templates() +SOLUTIONS = get_all_solutions() class SubmitRequest(BaseModel): code: str @@ -55,6 +56,18 @@ def get_task_details(task_id: str): "initial_code": template.get("initial_code", "# Write your code here.") } +@app.get("/api/tasks/{task_id}/solution") +def get_task_solution(task_id: str): + task = get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + + solution = SOLUTIONS.get(task_id) + if not solution: + raise HTTPException(status_code=404, detail="Solution not found") + + return solution + @app.post("/api/submit/{task_id}") def submit_code(task_id: str, request: SubmitRequest): result = execute_code(task_id, request.code) diff --git a/api/parser.py b/api/parser.py index ce0de6a..806cdae 100644 --- a/api/parser.py +++ b/api/parser.py @@ -45,6 +45,46 @@ def parse_notebook_template(filepath: str) -> dict: "initial_code": "# Error loading template code." } +def parse_notebook_solution(filepath: str) -> dict: + """ + Parses a Jupyter Notebook solution to extract explanatory text and the + reference solution code. + """ + try: + with open(filepath, 'r', encoding='utf-8') as f: + nb = json.load(f) + + solution_parts = [] + + for cell in nb.get("cells", []): + source = cell.get("source", []) + source_str = "".join(source).strip() + + if not source_str: + continue + + if cell["cell_type"] == "markdown": + filtered_source = [ + line for line in source + if "![Open In Colab]" not in line + ] + markdown = "".join(filtered_source).strip() + if markdown: + solution_parts.append(markdown) + + elif cell["cell_type"] == "code" and ("# ✅ SOLUTION" in source_str or "# SOLUTION" in source_str): + solution_parts.append(f"```python\n{source_str}\n```") + break + + return { + "solution": "\n\n".join(solution_parts) or "Solution not found in notebook." + } + except Exception as e: + print(f"Error parsing {filepath}: {e}") + return { + "solution": "Error loading solution." + } + def get_all_templates(templates_dir: str = "../templates") -> dict: """ Returns a dictionary mapping task_ids to their extracted template data. @@ -69,6 +109,26 @@ def get_all_templates(templates_dir: str = "../templates") -> dict: return templates +def get_all_solutions(solutions_dir: str = "../solutions") -> dict: + """ + Returns a dictionary mapping task_ids to their extracted solution data. + task_id is inferred from the filename (e.g., '01_relu_solution.ipynb' -> 'relu'). + """ + solutions = {} + + base_dir = os.path.dirname(os.path.abspath(__file__)) + solutions_path = os.path.join(base_dir, solutions_dir) + + for filepath in glob.glob(os.path.join(solutions_path, "*.ipynb")): + filename = os.path.basename(filepath) + + match = re.match(r"^\d+_(.+)_solution\.ipynb$", filename) + if match: + task_id = match.group(1) + solutions[task_id] = parse_notebook_solution(filepath) + + return solutions + if __name__ == "__main__": # Test parser res = get_all_templates() diff --git a/api/requirements.txt b/api/requirements.txt index c9b6004..540f3e1 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,3 +1,4 @@ fastapi uvicorn pydantic +torch diff --git a/web/package-lock.json b/web/package-lock.json index 42833dc..79e793a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -606,9 +606,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -625,9 +622,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -644,9 +638,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -663,9 +654,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -682,9 +670,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -701,9 +686,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -720,9 +702,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -739,9 +718,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -758,9 +734,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -783,9 +756,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -808,9 +778,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -833,9 +800,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -858,9 +822,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -883,9 +844,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -908,9 +866,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -933,9 +888,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1168,9 +1120,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1187,9 +1136,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1206,9 +1152,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1225,9 +1168,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1466,9 +1406,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1486,9 +1423,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1506,9 +1440,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1526,9 +1457,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2145,9 +2073,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2162,9 +2087,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2179,9 +2101,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2196,9 +2115,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2213,9 +2129,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2230,9 +2143,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2247,9 +2157,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2264,9 +2171,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5308,9 +5212,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5332,9 +5233,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5356,9 +5254,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5380,9 +5275,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index dfdec0d..65a4eda 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -48,6 +48,10 @@ type SubmissionResponse = { stderr: string; }; +type SolutionResponse = { + solution: string; +}; + export default function Home() { const [tasks, setTasks] = useState([]); const [selectedTaskId, setSelectedTaskId] = useState(null); @@ -58,6 +62,10 @@ export default function Home() { const [activeTab, setActiveTab] = useState<'problem' | 'results'>('problem'); const [solvedTasks, setSolvedTasks] = useState>({}); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [solution, setSolution] = useState(null); + const [isSolutionVisible, setIsSolutionVisible] = useState(false); + const [isSolutionLoading, setIsSolutionLoading] = useState(false); + const [solutionError, setSolutionError] = useState(null); useEffect(() => { // Load solved tasks from local storage @@ -92,6 +100,9 @@ export default function Home() { const savedCode = localStorage.getItem(`torchcode_code_${selectedTaskId}`); setCode(savedCode !== null ? savedCode : data.initial_code); setResults(null); + setSolution(null); + setSolutionError(null); + setIsSolutionVisible(false); setActiveTab('problem'); }) .catch(err => console.error("Failed to load task details", err)); @@ -138,6 +149,34 @@ export default function Home() { } }; + const handleSolutionToggle = async (isOpen: boolean) => { + if (!selectedTaskId) return; + + setIsSolutionVisible(isOpen); + if (!isOpen) { + return; + } + + setSolutionError(null); + + if (solution !== null) return; + + setIsSolutionLoading(true); + try { + const res = await fetch(`${API_BASE}/tasks/${selectedTaskId}/solution`); + if (!res.ok) { + throw new Error("Solution not found for this task."); + } + const data: SolutionResponse = await res.json(); + setSolution(data.solution); + } catch (err) { + console.error("Failed to load solution", err); + setSolutionError("Failed to load solution."); + } finally { + setIsSolutionLoading(false); + } + }; + const handleReset = () => { if (taskDetails && selectedTaskId) { localStorage.removeItem(`torchcode_code_${selectedTaskId}`); @@ -307,6 +346,39 @@ export default function Home() { )} +
+
{ + const isOpen = event.currentTarget.open; + if (isOpen !== isSolutionVisible) { + void handleSolutionToggle(isOpen); + } + }} + > + + Solution + + +
+ {isSolutionLoading ? ( +
Loading solution...
+ ) : solutionError ? ( +
+ {solutionError} +
+ ) : ( + + {solution || ""} + + )} +
+
+
) : (
Loading problem...
From f824c25e73c66e7c9f0867b7b6682737dfb687ee Mon Sep 17 00:00:00 2001 From: HenryHZY <168133331@qq.com> Date: Mon, 22 Jun 2026 12:20:58 +0800 Subject: [PATCH 2/4] update: store user solutions locally --- api/main.py | 32 ++++++++++++ user_solutions/README.md | 5 ++ user_solutions/cross_entropy.py | 21 ++++++++ user_solutions/dropout.py | 26 ++++++++++ user_solutions/gelu.py | 11 +++++ user_solutions/gradient_clipping.py | 30 +++++++++++ user_solutions/relu.py | 10 ++++ user_solutions/softmax.py | 23 +++++++++ user_solutions/weight_init.py | 14 ++++++ web/src/app/page.tsx | 77 ++++++++++++++++++++++++++--- 10 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 user_solutions/README.md create mode 100644 user_solutions/cross_entropy.py create mode 100644 user_solutions/dropout.py create mode 100644 user_solutions/gelu.py create mode 100644 user_solutions/gradient_clipping.py create mode 100644 user_solutions/relu.py create mode 100644 user_solutions/softmax.py create mode 100644 user_solutions/weight_init.py diff --git a/api/main.py b/api/main.py index 7f2661d..2f2642c 100644 --- a/api/main.py +++ b/api/main.py @@ -3,6 +3,7 @@ from pydantic import BaseModel import sys import os +from pathlib import Path # Add the root directory to PYTHONPATH so we can import torch_judge sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -25,10 +26,20 @@ # Load templates on startup TEMPLATES = get_all_templates() SOLUTIONS = get_all_solutions() +ROOT_DIR = Path(__file__).resolve().parent.parent +USER_CODE_DIR = ROOT_DIR / "user_solutions" class SubmitRequest(BaseModel): code: str +class CodeRequest(BaseModel): + code: str + +def get_user_code_path(task_id: str) -> Path: + if task_id not in TASKS: + raise HTTPException(status_code=404, detail="Task not found") + return USER_CODE_DIR / f"{task_id}.py" + @app.get("/api/tasks") def list_tasks(): tasks_list = [] @@ -68,6 +79,27 @@ def get_task_solution(task_id: str): return solution +@app.get("/api/code/{task_id}") +def get_saved_code(task_id: str): + code_path = get_user_code_path(task_id) + if not code_path.exists(): + return {"code": None, "saved": False} + return {"code": code_path.read_text(encoding="utf-8"), "saved": True} + +@app.put("/api/code/{task_id}") +def save_code(task_id: str, request: CodeRequest): + code_path = get_user_code_path(task_id) + USER_CODE_DIR.mkdir(parents=True, exist_ok=True) + code_path.write_text(request.code, encoding="utf-8") + return {"saved": True, "path": str(code_path.relative_to(ROOT_DIR))} + +@app.delete("/api/code/{task_id}") +def delete_saved_code(task_id: str): + code_path = get_user_code_path(task_id) + if code_path.exists(): + code_path.unlink() + return {"deleted": True} + @app.post("/api/submit/{task_id}") def submit_code(task_id: str, request: SubmitRequest): result = execute_code(task_id, request.code) diff --git a/user_solutions/README.md b/user_solutions/README.md new file mode 100644 index 0000000..c2b3eaa --- /dev/null +++ b/user_solutions/README.md @@ -0,0 +1,5 @@ +# User Solutions + +The standalone web UI saves edited code for each task in this directory as +`.py`. These files are intentionally tracked by Git so you can commit +and sync your work across machines. diff --git a/user_solutions/cross_entropy.py b/user_solutions/cross_entropy.py new file mode 100644 index 0000000..7159e1f --- /dev/null +++ b/user_solutions/cross_entropy.py @@ -0,0 +1,21 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def cross_entropy_loss(logits, targets): + # log_probs = logits - logsumexp(...) + + # to extract x_y based on the index from targets + batch_idx = torch.arange(logits.shape[0]) + x_y = logits[batch_idx, targets] + # or based on torch.gather + # x_y = torch.gather(logits, dim=-1, index=targets.unsqueeze(-1)).squeeze(-1) + + # cal sum_exp_log using torch.logsumexp + sum_exp_log = torch.logsumexp(logits, dim=-1, keepdim=True) + + logits = - (x_y - sum_exp_log) + return logits.mean() \ No newline at end of file diff --git a/user_solutions/dropout.py b/user_solutions/dropout.py new file mode 100644 index 0000000..9446c03 --- /dev/null +++ b/user_solutions/dropout.py @@ -0,0 +1,26 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +class MyDropout(nn.Module): + def __init__(self, p=0.5): + super().__init__() + + if not 0 <= p < 1: + raise ValueError("p must be in [0, 1)") + + self.p = p + + def forward(self, x): + if not self.training or self.p == 0: # or (self.p - 0)<1e-8 + return x + else: + # uniform distribution on the interval [0, 1) + drop_matrix = torch.rand(x.shape) + # drop_matrix = torch.rand_like(x) # 等价于上面的 + + mask = (drop_matrix>self.p).float() + return x*mask*(1/(1-self.p)) \ No newline at end of file diff --git a/user_solutions/gelu.py b/user_solutions/gelu.py new file mode 100644 index 0000000..3f74095 --- /dev/null +++ b/user_solutions/gelu.py @@ -0,0 +1,11 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def my_gelu(x): + out = x*0.5*(1+torch.erf(x/math.sqrt(2))) + # out = x*0.5*(1+torch.erf(x/torch.sqrt(2))) # Note: 2 is not a tensor + return out \ No newline at end of file diff --git a/user_solutions/gradient_clipping.py b/user_solutions/gradient_clipping.py new file mode 100644 index 0000000..116b60c --- /dev/null +++ b/user_solutions/gradient_clipping.py @@ -0,0 +1,30 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def clip_grad_norm(parameters, max_norm): + # compute total norm, clip if needed, return original norm + + # error: not all p has gradient + # sum_norm = torch.sqrt(sum(p.grad.norm()^2 for p in parameters)) + p_with_grad = [p for p in parameters if p.grad != None] + if p_with_grad == []: return torch.tensor(0.0) + + # l2 norm = 平方根 + # p.grad.norm()是tensor + total_norm = torch.sqrt(sum(p.grad.norm() ** 2 for p in p_with_grad)) + + clip_coef = max_norm/total_norm + + # 把 clip_coef 的最大值限制为 1.0(当total_norm>max_norm) + # clip_coef = torch.clamp(clip_coef, max=1.0) + if clip_coef < 1: + for p in parameters: + if p.grad != None: + p.grad*=clip_coef + # p.grad.mul_(clip_coef) # 等价 + + return total_norm diff --git a/user_solutions/relu.py b/user_solutions/relu.py new file mode 100644 index 0000000..8bc4832 --- /dev/null +++ b/user_solutions/relu.py @@ -0,0 +1,10 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def relu(x: torch.Tensor) -> torch.Tensor: + out = x * (x>0).float() + return out \ No newline at end of file diff --git a/user_solutions/softmax.py b/user_solutions/softmax.py new file mode 100644 index 0000000..d0c69d3 --- /dev/null +++ b/user_solutions/softmax.py @@ -0,0 +1,23 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def my_softmax(x: torch.Tensor, dim: int = -1) -> torch.Tensor: + # x could be 1D, 2D, ... + # if only 1D: + # exp_ = torch.exp(x - torch.max(x)) # cannot use max(x) + # sum_ = sum(exp_) + # out = exp_/sum_ + + # exp(x) != exp(x-max(x)), but softmax(exp(x)) = softmax(exp(x-max(x))) + + # torch.max(x)基本与x.max()等价; torch.sum(x)基本与x.sum()等价; + # exp_ = torch.exp(x - x.max(dim=dim, keepdim=True).values) + exp_ = torch.exp(x - torch.max(x, dim=dim, keepdim=True).values) + # sum_ = exp_.sum(dim=dim, keepdim=True) + sum_ = torch.sum(exp_, dim=dim, keepdim=True) + out = exp_/sum_ + return out diff --git a/user_solutions/weight_init.py b/user_solutions/weight_init.py new file mode 100644 index 0000000..5b7ce83 --- /dev/null +++ b/user_solutions/weight_init.py @@ -0,0 +1,14 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def kaiming_init(weight): + # fill with normal(0, sqrt(2/fan_in)) + fan_in = weight.shape[1] + std = math.sqrt(2/fan_in) + with torch.no_grad(): + weight.normal_(0, std) # in-place + return weight \ No newline at end of file diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 65a4eda..7b7d2dd 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import Editor from '@monaco-editor/react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -52,6 +52,11 @@ type SolutionResponse = { solution: string; }; +type SavedCodeResponse = { + code: string | null; + saved: boolean; +}; + export default function Home() { const [tasks, setTasks] = useState([]); const [selectedTaskId, setSelectedTaskId] = useState(null); @@ -66,6 +71,22 @@ export default function Home() { const [isSolutionVisible, setIsSolutionVisible] = useState(false); const [isSolutionLoading, setIsSolutionLoading] = useState(false); const [solutionError, setSolutionError] = useState(null); + const saveTimersRef = useRef>>({}); + + const saveCodeToDisk = (taskId: string, newCode: string) => { + const existingTimer = saveTimersRef.current[taskId]; + if (existingTimer) { + clearTimeout(existingTimer); + } + + saveTimersRef.current[taskId] = setTimeout(() => { + fetch(`${API_BASE}/code/${taskId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code: newCode }), + }).catch(err => console.error("Failed to save code to disk", err)); + }, 500); + }; useEffect(() => { // Load solved tasks from local storage @@ -91,21 +112,56 @@ export default function Home() { .catch(err => console.error("Failed to load tasks", err)); }, []); + useEffect(() => { + const timers = saveTimersRef.current; + return () => { + Object.values(timers).forEach(clearTimeout); + }; + }, []); + useEffect(() => { if (selectedTaskId) { - fetch(`${API_BASE}/tasks/${selectedTaskId}`) - .then(res => res.json()) - .then((data: TaskDetails) => { + let isCurrentTask = true; + + const loadTask = async () => { + try { + const taskRes = await fetch(`${API_BASE}/tasks/${selectedTaskId}`); + const data: TaskDetails = await taskRes.json(); + let savedCodeData: SavedCodeResponse = { code: null, saved: false }; + + try { + const savedCodeRes = await fetch(`${API_BASE}/code/${selectedTaskId}`); + savedCodeData = await savedCodeRes.json(); + } catch (err) { + console.error("Failed to load saved code from disk", err); + } + + if (!isCurrentTask) return; + setTaskDetails(data); const savedCode = localStorage.getItem(`torchcode_code_${selectedTaskId}`); - setCode(savedCode !== null ? savedCode : data.initial_code); + const codeToUse = savedCodeData.saved && savedCodeData.code !== null + ? savedCodeData.code + : savedCode !== null + ? savedCode + : data.initial_code; + + setCode(codeToUse); setResults(null); setSolution(null); setSolutionError(null); setIsSolutionVisible(false); setActiveTab('problem'); - }) - .catch(err => console.error("Failed to load task details", err)); + } catch (err) { + console.error("Failed to load task details", err); + } + }; + + loadTask(); + + return () => { + isCurrentTask = false; + }; } }, [selectedTaskId]); @@ -180,6 +236,12 @@ export default function Home() { const handleReset = () => { if (taskDetails && selectedTaskId) { localStorage.removeItem(`torchcode_code_${selectedTaskId}`); + const existingTimer = saveTimersRef.current[selectedTaskId]; + if (existingTimer) { + clearTimeout(existingTimer); + } + fetch(`${API_BASE}/code/${selectedTaskId}`, { method: "DELETE" }) + .catch(err => console.error("Failed to delete saved code", err)); setCode(taskDetails.initial_code); setResults(null); } @@ -503,6 +565,7 @@ export default function Home() { setCode(newCode); if (selectedTaskId) { localStorage.setItem(`torchcode_code_${selectedTaskId}`, newCode); + saveCodeToDisk(selectedTaskId, newCode); } }} options={{ From 862638358a48b8fdbcaa853fa015729c330e0ba0 Mon Sep 17 00:00:00 2001 From: HenryHZY <168133331@qq.com> Date: Wed, 24 Jun 2026 16:16:09 +0800 Subject: [PATCH 3/4] fix mha error & update my solution --- .../06_multihead_attention_solution.ipynb | 2 +- templates/06_multihead_attention.ipynb | 2 +- .../tasks/{mha.py => multihead_attention.py} | 0 user_solutions/attention.py | 17 +++++++++ user_solutions/batchnorm.py | 38 +++++++++++++++++++ user_solutions/conv2d.py | 37 ++++++++++++++++++ user_solutions/embedding.py | 17 +++++++++ user_solutions/layernorm.py | 22 +++++++++++ user_solutions/linear.py | 19 ++++++++++ user_solutions/mlp.py | 28 ++++++++++++++ user_solutions/rmsnorm.py | 17 +++++++++ 11 files changed, 197 insertions(+), 2 deletions(-) rename torch_judge/tasks/{mha.py => multihead_attention.py} (100%) create mode 100644 user_solutions/attention.py create mode 100644 user_solutions/batchnorm.py create mode 100644 user_solutions/conv2d.py create mode 100644 user_solutions/embedding.py create mode 100644 user_solutions/layernorm.py create mode 100644 user_solutions/linear.py create mode 100644 user_solutions/mlp.py create mode 100644 user_solutions/rmsnorm.py diff --git a/solutions/06_multihead_attention_solution.ipynb b/solutions/06_multihead_attention_solution.ipynb index 7ed4ad4..35d4bc6 100644 --- a/solutions/06_multihead_attention_solution.ipynb +++ b/solutions/06_multihead_attention_solution.ipynb @@ -100,7 +100,7 @@ "source": [ "# Run judge\n", "from torch_judge import check\n", - "check(\"mha\")" + "check(\"multihead_attention\")" ] } ], diff --git a/templates/06_multihead_attention.ipynb b/templates/06_multihead_attention.ipynb index 7812714..6785a70 100644 --- a/templates/06_multihead_attention.ipynb +++ b/templates/06_multihead_attention.ipynb @@ -112,7 +112,7 @@ "source": [ "# ✅ SUBMIT\n", "from torch_judge import check\n", - "check(\"mha\")" + "check(\"multihead_attention\")" ] } ], diff --git a/torch_judge/tasks/mha.py b/torch_judge/tasks/multihead_attention.py similarity index 100% rename from torch_judge/tasks/mha.py rename to torch_judge/tasks/multihead_attention.py diff --git a/user_solutions/attention.py b/user_solutions/attention.py new file mode 100644 index 0000000..ad478df --- /dev/null +++ b/user_solutions/attention.py @@ -0,0 +1,17 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def scaled_dot_product_attention(Q, K, V): + x = Q @ K.transpose(-2, -1) + # or: x = Q @ K.transpose(-1, -2), 调换dim=-1和dim=-2 + # or: x = Q @ K.mT, mT可以调换最后两个dim,T会调换所有dim + x = x/math.sqrt(K.shape[-1]) + # shape = (batch, seq_q, seq_k) + attn = torch.softmax(x, dim=-1) + # shape = (batch, seq_q, seq_k) + out = attn @ V + return out \ No newline at end of file diff --git a/user_solutions/batchnorm.py b/user_solutions/batchnorm.py new file mode 100644 index 0000000..b2880ad --- /dev/null +++ b/user_solutions/batchnorm.py @@ -0,0 +1,38 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def my_batch_norm( + x, + gamma, + beta, + running_mean, + running_var, + eps=1e-5, + momentum=0.1, + training=True, +): + # x.shape = (B, D) + # gamma.shape = beta.shape = (B, D) + if training: + # error: 不能加keepdim=True,后面的updated in-place会报错 + mean = torch.mean(x, dim=0) # (D) + var = torch.var(x, dim=0, unbiased=False) # (D) + + with torch.no_grad(): + # error: 没有updated in-place + # running_mean = (1 - momentum) * running_mean + momentum * mean + # running_var = (1 - momentum) * running_var + momentum * var + running_mean.mul_(1 - momentum).add_(momentum * mean) + running_var.mul_(1 - momentum).add_(momentum * var) + else: + mean = running_mean + var = running_var + + norm = (x-mean)/torch.sqrt(var+eps) + return gamma*norm+beta + + diff --git a/user_solutions/conv2d.py b/user_solutions/conv2d.py new file mode 100644 index 0000000..b84abc4 --- /dev/null +++ b/user_solutions/conv2d.py @@ -0,0 +1,37 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def my_conv2d(x, weight, bias=None, stride=1, padding=0): # no dilation (i.e., dilation=1) + # extract patches, apply kernel, handle stride/padding + # x: (B, C_in, H, W), weight: (C_out, C_in, kH, kW), bias: None or (C_out) + # Returns: (B, C_out, H_out, W_out) + B, C_in, H, W = x.shape + C_out, _, kH, kW = weight.shape + H_padded = H+2*padding + W_padded = W+2*padding + H_out = (H_padded - kH) // stride + 1 + W_out = (W_padded - kW) // stride + 1 + + # F.unfold can handle pading & stride + # extract patches using F.unfold(input, kernel_size, dilation=1, padding=0, stride=1) + patches = F.unfold(x, kernel_size=(kH, kW), padding=padding, stride=stride) + # patches.shape = (B, C_in * kH * kW, H_out * W_out) + + # apply kernel + weight = weight.view(C_out, C_in * kH * kW) + # or: weight = weight.view(C_out, -1), 说明dim=0是C_out、dim=1是-1即自动推断 + out = weight @ patches + # out.shape = (B, C_out, H_out * W_out) + out = out.view(B, C_out, H_out, W_out) + # if bias: erro: 多元素 Tensor 不能直接转成布尔值 + if bias is not None: + bias = bias.view(1, C_out, 1, 1) # shape = C_out -> 1*C_out*1*1 + out += bias + return out + + + \ No newline at end of file diff --git a/user_solutions/embedding.py b/user_solutions/embedding.py new file mode 100644 index 0000000..6d4ff34 --- /dev/null +++ b/user_solutions/embedding.py @@ -0,0 +1,17 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +class MyEmbedding(nn.Module): + def __init__(self, num_embeddings, embedding_dim): + super().__init__() + + # trainable: use nn.Parameter instead of simple tensor/matrix + # nn.Parameter需要传入tensor,而不仅仅是shape + self.weight = nn.Parameter(torch.rand(num_embeddings, embedding_dim)) + + def forward(self, indices): + return self.weight[indices] \ No newline at end of file diff --git a/user_solutions/layernorm.py b/user_solutions/layernorm.py new file mode 100644 index 0000000..c86f088 --- /dev/null +++ b/user_solutions/layernorm.py @@ -0,0 +1,22 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def my_layer_norm(x, gamma, beta, eps=1e-5): + # x.shape = (B, D) + # gamma.shape = beta.shape = (B, D) + + mean = torch.mean(x, dim=-1, keepdim=True) # (B, 1) + + # Layernorm的std or var需要用unbiased=False + # std = torch.std(x, dim=-1, keepdim=True) # (B, 1) + # std = torch.std(x, dim=-1, keepdim=True, unbiased=False) # (B, 1) + # out = gamma*(x-mean)/torch.sqrt(std**2 + eps) + beta + # or + var = torch.var(x, dim=-1, keepdim=True, unbiased=False) # (B, 1) + out = gamma*(x-mean)/torch.sqrt(var + eps) + beta + + return out \ No newline at end of file diff --git a/user_solutions/linear.py b/user_solutions/linear.py new file mode 100644 index 0000000..f831557 --- /dev/null +++ b/user_solutions/linear.py @@ -0,0 +1,19 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +class SimpleLinear: + def __init__(self, in_features: int, out_features: int): + # Initialize weight and bias + self.weight = nn.Parameter(torch.rand(out_features, in_features)*(1/math.sqrt(in_features))) + self.bias = nn.Parameter(torch.zeros(out_features)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Compute y = x @ W^T + b + # A @ B:矩阵乘法(matmul) + # A * B:逐元素乘(element-wise / Hadamard 乘积) + y = x@self.weight.T + self.bias + return y \ No newline at end of file diff --git a/user_solutions/mlp.py b/user_solutions/mlp.py new file mode 100644 index 0000000..b55fbd0 --- /dev/null +++ b/user_solutions/mlp.py @@ -0,0 +1,28 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +class SwiGLUMLP(nn.Module): + def __init__(self, d_model, d_ff): + super().__init__() + # Initialize gate_proj, up_proj, down_proj + self.up_proj = nn.Linear(d_model, d_ff) + self.gate_proj = nn.Linear(d_model, d_ff) + self.down_proj = nn.Linear(d_ff, d_model) + + def my_silu(self, x): + return x*torch.sigmoid(x) + + def forward(self, x): + # down_proj(silu(gate_proj(x)) * up_proj(x)) + up = self.up_proj(x) + + gate = F.silu(self.gate_proj(x)) + # or: + # gate = self.my_silu(self.gate_proj(x)) + + down = self.down_proj(gate*up) # element-wise product + return down \ No newline at end of file diff --git a/user_solutions/rmsnorm.py b/user_solutions/rmsnorm.py new file mode 100644 index 0000000..42f21c3 --- /dev/null +++ b/user_solutions/rmsnorm.py @@ -0,0 +1,17 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +# ✏️ YOUR IMPLEMENTATION HERE + +def rms_norm(x, weight, eps=1e-6): + # x.shape = (B, D) + # weight.shape = (D) + rms = torch.sqrt(1/x.shape[-1]*torch.sum(x**2, dim=-1, keepdim=True) + eps) # (B, 1) + # or: rms = torch.sqrt(x.pow(2).mean(dim=-1, keepdim=True) + eps) + + # error: RMSNorm用的weight需要element-wise product + # rms_norm = x/rms @ weight + rms_norm = x/rms * weight + return rms_norm \ No newline at end of file From 4a09cb75f1722e35f278b0142bd9f20f37f58e85 Mon Sep 17 00:00:00 2001 From: HenryHZY <168133331@qq.com> Date: Wed, 24 Jun 2026 16:32:19 +0800 Subject: [PATCH 4/4] remove user_solutions --- README.md | 6 ++-- START_WEB_UI.md | 48 ----------------------------- user_solutions/README.md | 5 --- user_solutions/attention.py | 17 ---------- user_solutions/batchnorm.py | 38 ----------------------- user_solutions/conv2d.py | 37 ---------------------- user_solutions/cross_entropy.py | 21 ------------- user_solutions/dropout.py | 26 ---------------- user_solutions/embedding.py | 17 ---------- user_solutions/gelu.py | 11 ------- user_solutions/gradient_clipping.py | 30 ------------------ user_solutions/layernorm.py | 22 ------------- user_solutions/linear.py | 19 ------------ user_solutions/mlp.py | 28 ----------------- user_solutions/relu.py | 10 ------ user_solutions/rmsnorm.py | 17 ---------- user_solutions/softmax.py | 23 -------------- user_solutions/weight_init.py | 14 --------- 18 files changed, 3 insertions(+), 386 deletions(-) delete mode 100644 START_WEB_UI.md delete mode 100644 user_solutions/README.md delete mode 100644 user_solutions/attention.py delete mode 100644 user_solutions/batchnorm.py delete mode 100644 user_solutions/conv2d.py delete mode 100644 user_solutions/cross_entropy.py delete mode 100644 user_solutions/dropout.py delete mode 100644 user_solutions/embedding.py delete mode 100644 user_solutions/gelu.py delete mode 100644 user_solutions/gradient_clipping.py delete mode 100644 user_solutions/layernorm.py delete mode 100644 user_solutions/linear.py delete mode 100644 user_solutions/mlp.py delete mode 100644 user_solutions/relu.py delete mode 100644 user_solutions/rmsnorm.py delete mode 100644 user_solutions/softmax.py delete mode 100644 user_solutions/weight_init.py diff --git a/README.md b/README.md index c2504d3..2b2fd2e 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ For a modern, standalone coding experience with an integrated IDE and dual-pane ``` 2. **Start Frontend (Next.js):** ```bash -cd web -npm install -npm run dev + cd web + npm install + npm run dev ``` 3. Open **** in your browser. diff --git a/START_WEB_UI.md b/START_WEB_UI.md deleted file mode 100644 index a6ebfc0..0000000 --- a/START_WEB_UI.md +++ /dev/null @@ -1,48 +0,0 @@ -# Start Standalone Web UI - -## First-Time Setup - -Run these from the project root: - -```bash -python -m pip install -r api/requirements.txt -cd web && npm install -``` - -## Start Backend - -Terminal 1, from the project root: - -```bash -python -m uvicorn api.main:app --port 8000 --reload -``` - -## Start Frontend - -Terminal 2, from the project root: - -```bash -cd web && WATCHPACK_POLLING=true NEXT_TELEMETRY_DISABLED=1 npm run dev -- --hostname 127.0.0.1 --port 3000 -``` - -Open: - -```text -http://127.0.0.1:3000/ -``` - -## Stop Servers - -Press `Ctrl+C` in both terminals, or run: - -```bash -lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill -lsof -tiTCP:8000 -sTCP:LISTEN | xargs -r kill -``` - -## Quick Checks - -```bash -curl -I http://127.0.0.1:3000 -curl -I http://127.0.0.1:8000/docs -``` diff --git a/user_solutions/README.md b/user_solutions/README.md deleted file mode 100644 index c2b3eaa..0000000 --- a/user_solutions/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# User Solutions - -The standalone web UI saves edited code for each task in this directory as -`.py`. These files are intentionally tracked by Git so you can commit -and sync your work across machines. diff --git a/user_solutions/attention.py b/user_solutions/attention.py deleted file mode 100644 index ad478df..0000000 --- a/user_solutions/attention.py +++ /dev/null @@ -1,17 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def scaled_dot_product_attention(Q, K, V): - x = Q @ K.transpose(-2, -1) - # or: x = Q @ K.transpose(-1, -2), 调换dim=-1和dim=-2 - # or: x = Q @ K.mT, mT可以调换最后两个dim,T会调换所有dim - x = x/math.sqrt(K.shape[-1]) - # shape = (batch, seq_q, seq_k) - attn = torch.softmax(x, dim=-1) - # shape = (batch, seq_q, seq_k) - out = attn @ V - return out \ No newline at end of file diff --git a/user_solutions/batchnorm.py b/user_solutions/batchnorm.py deleted file mode 100644 index b2880ad..0000000 --- a/user_solutions/batchnorm.py +++ /dev/null @@ -1,38 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def my_batch_norm( - x, - gamma, - beta, - running_mean, - running_var, - eps=1e-5, - momentum=0.1, - training=True, -): - # x.shape = (B, D) - # gamma.shape = beta.shape = (B, D) - if training: - # error: 不能加keepdim=True,后面的updated in-place会报错 - mean = torch.mean(x, dim=0) # (D) - var = torch.var(x, dim=0, unbiased=False) # (D) - - with torch.no_grad(): - # error: 没有updated in-place - # running_mean = (1 - momentum) * running_mean + momentum * mean - # running_var = (1 - momentum) * running_var + momentum * var - running_mean.mul_(1 - momentum).add_(momentum * mean) - running_var.mul_(1 - momentum).add_(momentum * var) - else: - mean = running_mean - var = running_var - - norm = (x-mean)/torch.sqrt(var+eps) - return gamma*norm+beta - - diff --git a/user_solutions/conv2d.py b/user_solutions/conv2d.py deleted file mode 100644 index b84abc4..0000000 --- a/user_solutions/conv2d.py +++ /dev/null @@ -1,37 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def my_conv2d(x, weight, bias=None, stride=1, padding=0): # no dilation (i.e., dilation=1) - # extract patches, apply kernel, handle stride/padding - # x: (B, C_in, H, W), weight: (C_out, C_in, kH, kW), bias: None or (C_out) - # Returns: (B, C_out, H_out, W_out) - B, C_in, H, W = x.shape - C_out, _, kH, kW = weight.shape - H_padded = H+2*padding - W_padded = W+2*padding - H_out = (H_padded - kH) // stride + 1 - W_out = (W_padded - kW) // stride + 1 - - # F.unfold can handle pading & stride - # extract patches using F.unfold(input, kernel_size, dilation=1, padding=0, stride=1) - patches = F.unfold(x, kernel_size=(kH, kW), padding=padding, stride=stride) - # patches.shape = (B, C_in * kH * kW, H_out * W_out) - - # apply kernel - weight = weight.view(C_out, C_in * kH * kW) - # or: weight = weight.view(C_out, -1), 说明dim=0是C_out、dim=1是-1即自动推断 - out = weight @ patches - # out.shape = (B, C_out, H_out * W_out) - out = out.view(B, C_out, H_out, W_out) - # if bias: erro: 多元素 Tensor 不能直接转成布尔值 - if bias is not None: - bias = bias.view(1, C_out, 1, 1) # shape = C_out -> 1*C_out*1*1 - out += bias - return out - - - \ No newline at end of file diff --git a/user_solutions/cross_entropy.py b/user_solutions/cross_entropy.py deleted file mode 100644 index 7159e1f..0000000 --- a/user_solutions/cross_entropy.py +++ /dev/null @@ -1,21 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def cross_entropy_loss(logits, targets): - # log_probs = logits - logsumexp(...) - - # to extract x_y based on the index from targets - batch_idx = torch.arange(logits.shape[0]) - x_y = logits[batch_idx, targets] - # or based on torch.gather - # x_y = torch.gather(logits, dim=-1, index=targets.unsqueeze(-1)).squeeze(-1) - - # cal sum_exp_log using torch.logsumexp - sum_exp_log = torch.logsumexp(logits, dim=-1, keepdim=True) - - logits = - (x_y - sum_exp_log) - return logits.mean() \ No newline at end of file diff --git a/user_solutions/dropout.py b/user_solutions/dropout.py deleted file mode 100644 index 9446c03..0000000 --- a/user_solutions/dropout.py +++ /dev/null @@ -1,26 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -class MyDropout(nn.Module): - def __init__(self, p=0.5): - super().__init__() - - if not 0 <= p < 1: - raise ValueError("p must be in [0, 1)") - - self.p = p - - def forward(self, x): - if not self.training or self.p == 0: # or (self.p - 0)<1e-8 - return x - else: - # uniform distribution on the interval [0, 1) - drop_matrix = torch.rand(x.shape) - # drop_matrix = torch.rand_like(x) # 等价于上面的 - - mask = (drop_matrix>self.p).float() - return x*mask*(1/(1-self.p)) \ No newline at end of file diff --git a/user_solutions/embedding.py b/user_solutions/embedding.py deleted file mode 100644 index 6d4ff34..0000000 --- a/user_solutions/embedding.py +++ /dev/null @@ -1,17 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -class MyEmbedding(nn.Module): - def __init__(self, num_embeddings, embedding_dim): - super().__init__() - - # trainable: use nn.Parameter instead of simple tensor/matrix - # nn.Parameter需要传入tensor,而不仅仅是shape - self.weight = nn.Parameter(torch.rand(num_embeddings, embedding_dim)) - - def forward(self, indices): - return self.weight[indices] \ No newline at end of file diff --git a/user_solutions/gelu.py b/user_solutions/gelu.py deleted file mode 100644 index 3f74095..0000000 --- a/user_solutions/gelu.py +++ /dev/null @@ -1,11 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def my_gelu(x): - out = x*0.5*(1+torch.erf(x/math.sqrt(2))) - # out = x*0.5*(1+torch.erf(x/torch.sqrt(2))) # Note: 2 is not a tensor - return out \ No newline at end of file diff --git a/user_solutions/gradient_clipping.py b/user_solutions/gradient_clipping.py deleted file mode 100644 index 116b60c..0000000 --- a/user_solutions/gradient_clipping.py +++ /dev/null @@ -1,30 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def clip_grad_norm(parameters, max_norm): - # compute total norm, clip if needed, return original norm - - # error: not all p has gradient - # sum_norm = torch.sqrt(sum(p.grad.norm()^2 for p in parameters)) - p_with_grad = [p for p in parameters if p.grad != None] - if p_with_grad == []: return torch.tensor(0.0) - - # l2 norm = 平方根 - # p.grad.norm()是tensor - total_norm = torch.sqrt(sum(p.grad.norm() ** 2 for p in p_with_grad)) - - clip_coef = max_norm/total_norm - - # 把 clip_coef 的最大值限制为 1.0(当total_norm>max_norm) - # clip_coef = torch.clamp(clip_coef, max=1.0) - if clip_coef < 1: - for p in parameters: - if p.grad != None: - p.grad*=clip_coef - # p.grad.mul_(clip_coef) # 等价 - - return total_norm diff --git a/user_solutions/layernorm.py b/user_solutions/layernorm.py deleted file mode 100644 index c86f088..0000000 --- a/user_solutions/layernorm.py +++ /dev/null @@ -1,22 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def my_layer_norm(x, gamma, beta, eps=1e-5): - # x.shape = (B, D) - # gamma.shape = beta.shape = (B, D) - - mean = torch.mean(x, dim=-1, keepdim=True) # (B, 1) - - # Layernorm的std or var需要用unbiased=False - # std = torch.std(x, dim=-1, keepdim=True) # (B, 1) - # std = torch.std(x, dim=-1, keepdim=True, unbiased=False) # (B, 1) - # out = gamma*(x-mean)/torch.sqrt(std**2 + eps) + beta - # or - var = torch.var(x, dim=-1, keepdim=True, unbiased=False) # (B, 1) - out = gamma*(x-mean)/torch.sqrt(var + eps) + beta - - return out \ No newline at end of file diff --git a/user_solutions/linear.py b/user_solutions/linear.py deleted file mode 100644 index f831557..0000000 --- a/user_solutions/linear.py +++ /dev/null @@ -1,19 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -class SimpleLinear: - def __init__(self, in_features: int, out_features: int): - # Initialize weight and bias - self.weight = nn.Parameter(torch.rand(out_features, in_features)*(1/math.sqrt(in_features))) - self.bias = nn.Parameter(torch.zeros(out_features)) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - # Compute y = x @ W^T + b - # A @ B:矩阵乘法(matmul) - # A * B:逐元素乘(element-wise / Hadamard 乘积) - y = x@self.weight.T + self.bias - return y \ No newline at end of file diff --git a/user_solutions/mlp.py b/user_solutions/mlp.py deleted file mode 100644 index b55fbd0..0000000 --- a/user_solutions/mlp.py +++ /dev/null @@ -1,28 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -class SwiGLUMLP(nn.Module): - def __init__(self, d_model, d_ff): - super().__init__() - # Initialize gate_proj, up_proj, down_proj - self.up_proj = nn.Linear(d_model, d_ff) - self.gate_proj = nn.Linear(d_model, d_ff) - self.down_proj = nn.Linear(d_ff, d_model) - - def my_silu(self, x): - return x*torch.sigmoid(x) - - def forward(self, x): - # down_proj(silu(gate_proj(x)) * up_proj(x)) - up = self.up_proj(x) - - gate = F.silu(self.gate_proj(x)) - # or: - # gate = self.my_silu(self.gate_proj(x)) - - down = self.down_proj(gate*up) # element-wise product - return down \ No newline at end of file diff --git a/user_solutions/relu.py b/user_solutions/relu.py deleted file mode 100644 index 8bc4832..0000000 --- a/user_solutions/relu.py +++ /dev/null @@ -1,10 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def relu(x: torch.Tensor) -> torch.Tensor: - out = x * (x>0).float() - return out \ No newline at end of file diff --git a/user_solutions/rmsnorm.py b/user_solutions/rmsnorm.py deleted file mode 100644 index 42f21c3..0000000 --- a/user_solutions/rmsnorm.py +++ /dev/null @@ -1,17 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def rms_norm(x, weight, eps=1e-6): - # x.shape = (B, D) - # weight.shape = (D) - rms = torch.sqrt(1/x.shape[-1]*torch.sum(x**2, dim=-1, keepdim=True) + eps) # (B, 1) - # or: rms = torch.sqrt(x.pow(2).mean(dim=-1, keepdim=True) + eps) - - # error: RMSNorm用的weight需要element-wise product - # rms_norm = x/rms @ weight - rms_norm = x/rms * weight - return rms_norm \ No newline at end of file diff --git a/user_solutions/softmax.py b/user_solutions/softmax.py deleted file mode 100644 index d0c69d3..0000000 --- a/user_solutions/softmax.py +++ /dev/null @@ -1,23 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def my_softmax(x: torch.Tensor, dim: int = -1) -> torch.Tensor: - # x could be 1D, 2D, ... - # if only 1D: - # exp_ = torch.exp(x - torch.max(x)) # cannot use max(x) - # sum_ = sum(exp_) - # out = exp_/sum_ - - # exp(x) != exp(x-max(x)), but softmax(exp(x)) = softmax(exp(x-max(x))) - - # torch.max(x)基本与x.max()等价; torch.sum(x)基本与x.sum()等价; - # exp_ = torch.exp(x - x.max(dim=dim, keepdim=True).values) - exp_ = torch.exp(x - torch.max(x, dim=dim, keepdim=True).values) - # sum_ = exp_.sum(dim=dim, keepdim=True) - sum_ = torch.sum(exp_, dim=dim, keepdim=True) - out = exp_/sum_ - return out diff --git a/user_solutions/weight_init.py b/user_solutions/weight_init.py deleted file mode 100644 index 5b7ce83..0000000 --- a/user_solutions/weight_init.py +++ /dev/null @@ -1,14 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math - -# ✏️ YOUR IMPLEMENTATION HERE - -def kaiming_init(weight): - # fill with normal(0, sqrt(2/fan_in)) - fan_in = weight.shape[1] - std = math.sqrt(2/fan_in) - with torch.no_grad(): - weight.normal_(0, std) # in-place - return weight \ No newline at end of file