ml-server-runtime
ML 탭 pyodide 버리고 서버 사이드 python 실행으로 갈아탐
ML problem tab execution flow
2026-06부터 브라우저 pyodide 안 쓰고 서버에서 돌림.
lib/run/ml-client.ts (POST) → /api/run → lib/run/ml.ts runMlProblem()
- user.py, tests.py, viz.py, harness.py 임시 디렉토리에 생성
python3 harness.py실행- harness.py가 user code → tests → viz (matplotlib AGG) 순서로 돌림
- 결과는 JSON 한 줄로 반환
{stdout, tests, viz, error}
matplotlib은 viz 실행할 때만 lazy import 해서 평소엔 numpy만 쓰고 빠름.
Sandbox & Limits
lib/run/spawn.ts 공유해서 사용.
- RUN_MS=25_000: sklearn/matplotlib 임포트 시간 때문에 넉넉하게 잡음
- maxOut=8_000_000: viz 결과물(base64 PNG) 용량 때문에 기본 50KB 쓰면 터짐. 이거 안 맞추면 JSON.parse 에러 남. wtf
Runner Image Setup
Dockerfile에서 alpine 패키지로 미리 다 깔아둠 (scipy/sklearn 컴파일 지옥 피하기용).
apk add py3-numpy py3-scipy py3-matplotlib py3-scikit-learn
ENV MPLCONFIGDIR=/tmp/matplotlib
- MPLCONFIGDIR: node 유저 홈 디렉토리 쓰기 권한 없어서 임시 폴더로 지정
- docker-compose mem_limit: 1500m -> 3g로 증설 (python 피크 타임 대비)
ML problem 파일 구조(meta.json, starter.py 등)는 그대로임. 실행 위치만 바뀜.
여기서 배울 것
- viz 결과물 base64 용량 때문에 maxOut 크게 잡아야 함
- scipy/sklearn은 alpine prebuilt 패키지로 쓰는 게 정신건강에 좋음
- matplotlib 쓰려면 MPLCONFIGDIR 설정 필수
원본 파일 보기 (.claude/projects/-home-son-prj-learn-code-test/memory/ml-server-runtime.md)
---
name: ml-server-runtime
description: learn_code_test/fe ML tab runs Python server-side via /api/run (not pyodide) since 2026-06
metadata:
node_type: memory
type: project
originSessionId: 3d295a8c-6b6d-4d99-b0aa-e6026af86fd2
---
The ML problem tab (`fe/problems/*`, `ProblemWorkspace.tsx`) executes server-side, NOT in-browser pyodide (changed 2026-06-28). Flow: `lib/run/ml-client.ts` POSTs `{userCode, testsCode, vizCode}` to `/api/run` → the route (`app/api/run/route.ts`) branches on `typeof body.userCode === "string"` → `lib/run/ml.ts` `runMlProblem()` writes user.py/tests.py/viz.py + harness.py to a temp dir and spawns `python3 harness.py`. The harness (`lib/run/ml-harness.ts`, a `String.raw` Python source) is a server port of the old pyodide worker: runs user code, then `run_tests(ns)`, then (only if all pass) `run_visualize(ns)` → matplotlib AGG → base64 PNG data URI; returns one JSON line `{stdout, tests, viz, error}`. matplotlib is imported lazily (only in the viz branch) so typical runs are numpy-only and fast. `_ml_print` rewrites `print(x)` → `x = <val>` exactly like before.
Spawn sandbox is shared with the C/Java/Python quiz runner via `lib/run/spawn.ts` (`runStep(cmd, cwd, timeoutMs, maxOut)` — detached process group, `ulimit -t/-f`, wall-clock SIGKILL, per-stream byte cap; `cap()`). ML uses `RUN_MS=25_000` (cold sklearn/matplotlib imports) and `maxOut=8_000_000` (the viz base64 PNG would blow the default 50KB cap and break JSON.parse — that was the key gotcha). exec.ts (lang runner) still uses the default cap.
Runner image deps: Dockerfile runner stage `apk add py3-numpy py3-scipy py3-matplotlib py3-scikit-learn` (Alpine prebuilt — avoids compiling scipy/sklearn) + `ENV MPLCONFIGDIR=/tmp/matplotlib` (node user's home isn't writable). docker-compose `mem_limit` raised 1500m→3g (each python peaks a few hundred MB). Verified end-to-end inside `node:22-alpine` with these packages: numpy 2.4.6 / scipy 1.17.1 / sklearn 1.5.2 / matplotlib 3.10.6, viz PNG ~30KB. The ML problem file structure (meta.json/description.md/starter.py/solution.py/tests.py/visualize.py, `run_tests`/`run_visualize` contracts) is UNCHANGED — only the execution location moved. Related: [[runtime-tab-pattern]].