← 전체로 돌아가기
프로젝트 메모 python -home-son-prj-learn-code-test

ml-server-runtime

ML 탭 pyodide 버리고 서버 사이드 python 실행으로 갈아탐

pythonmldockerruntime

ML problem tab execution flow

2026-06부터 브라우저 pyodide 안 쓰고 서버에서 돌림.

lib/run/ml-client.ts (POST) → /api/runlib/run/ml.ts runMlProblem()

  1. user.py, tests.py, viz.py, harness.py 임시 디렉토리에 생성
  2. python3 harness.py 실행
  3. harness.py가 user code → tests → viz (matplotlib AGG) 순서로 돌림
  4. 결과는 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 등)는 그대로임. 실행 위치만 바뀜.

여기서 배울 것

  1. viz 결과물 base64 용량 때문에 maxOut 크게 잡아야 함
  2. scipy/sklearn은 alpine prebuilt 패키지로 쓰는 게 정신건강에 좋음
  3. 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]].