
รัน Local LLM ช่วยเขียนเว็บ แล้วเจอ OOM: บันทึกปัญหาจากสนามจริง
ปกติเวลาเราใช้ AI ช่วยเขียนโค้ด เรามักจะใช้ผ่าน API หรือหน้าเว็บ ซึ่งสะดวกและมี resource ให้ใช้เหลือเฟือ แต่ครั้งนี้ผมอยากลองของ โดยเปลี่ยนมารัน LLM บนเครื่อง Mac ของตัวเองดูบ้าง อยากรู้ว่ามันจะพาโปรเจกต์เดินจากศูนย์ไปจนขึ้นออนไลน์ได้จริงไหม
โจทย์คือสั่งประโยคเดียวสั้นๆ: “สร้างเว็บไซต์ท่องเที่ยว จังหวัดใดก็ได้ที่มีทะเล รูปประกอบเป็นตัวการ์ตูนน่ารักๆ และสร้างรูปด้วย codex cli”
การใช้ Local LLM มีข้อดีคือ โค้ดกับข้อมูลไม่ต้องออกจากเครื่อง ไม่ต้องจ่ายค่า API และเราสามารถมอนิเตอร์การทำงานของระบบได้ทั้งหมด ซึ่งเครื่องมือที่ผมใช้มี mlx_lm สำหรับรันโมเดลฝั่งเซิร์ฟเวอร์, opencode สำหรับรับคำสั่งมาเขียนไฟล์จริงในโปรเจกต์ และ codex CLI สำหรับเจนรูปการ์ตูนประกอบเว็บอย่างเดียว ไม่ได้แตะโค้ด
การติดตั้งและการรัน Local LLM ด้วย MLX
สำหรับคนใช้ Mac ที่เป็น Apple Silicon ตัวเลือกที่ดีที่สุดตอนนี้คือการใช้ MLX ของ Apple เอง เพราะมันถูกรีดประสิทธิภาพมาเพื่อชิปของ Mac โดยเฉพาะ
เริ่มจากการติดตั้ง library ที่จำเป็น (แนะนำให้สร้าง virtual environment ก่อน):
pip install mlx-lm
หลายคนอาจจะสงสัยว่าสเปคเครื่องตัวเองรันโมเดลตัวไหนได้บ้าง? ผมแนะนำให้ลองเข้าไปที่เว็บ llm-cal.p7z.pw ซึ่งเป็น VRAM calculator ให้กรอกสเปคเครื่องแล้วคำนวณ memory, KV cache และ throughput ที่จะได้ พร้อมแนะนำโมเดลที่เหมาะสม สำหรับเครื่องของผม ระบบแนะนำว่าสามารถรันโมเดลใหญ่อย่าง Qwen3.8-27B ได้สบายๆ
จากนั้นเราจะรันโมเดลเป็น HTTP server เพื่อให้ opencode เรียกใช้ได้ผ่าน API โดยเลือกใช้โมเดลที่ผ่านการทำ Quantization (ลดขนาด) เป็น 4-bit เพื่อประหยัด RAM:
export MLX_MODEL="mlx-community/Qwen3.8-27B-4bit"
python -m mlx_lm.server --model "$MLX_MODEL" --host 127.0.0.1 --port 8080
เพียงเท่านี้เราก็จะได้ Local LLM พร้อมใช้งานบน http://127.0.0.1:8080
ปัญหาคลาสสิก: รันไปสักพักแล้ว OOM (Out of Memory)

เครื่องที่ผมใช้รันคือ MacBook Pro 16” (M5 Pro, 18-core CPU / 20-core GPU, RAM 48GB) ซึ่งแม้สเปคจะแรงแค่ไหน แต่ในช่วงแรกที่ให้โมเดลช่วยร่างโครงเว็บ ทุกอย่างไหลลื่นดีมาก พอเริ่มสั่งงานต่อเนื่องยาวๆ ปรากฏว่า RAM Usage พุ่งทะลุไปถึง 38GB ในบางครั้ง — เหลือที่ว่างอีกไม่ถึง 10GB จาก 48GB ที่มี สุดท้ายเซิร์ฟเวอร์ก็หยุดนิ่งไปเฉยๆ และแสดง Error ใน Terminal แบบนี้:
Exception in thread Thread-1 (_generate):
Traceback (most recent call last):
File "/opt/homebrew/Cellar/[email protected]/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/threading.py", line 1082, in _bootstrap_inner
self._context.run(self.run)
File "/opt/homebrew/Cellar/[email protected]/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/threading.py", line 1024, in run
self._target(*self._args, **self._kwargs)
File "/Users/myuser/.venvs/mlx/lib/python3.14/site-packages/mlx_lm/server.py", line 853, in _generate
prompt_responses, gen_responses = batch_generator.next()
File "/Users/myuser/.venvs/mlx/lib/python3.14/site-packages/mlx_lm/generate.py", line 1855, in next
return self._next()
File "/Users/myuser/.venvs/mlx/lib/python3.14/site-packages/mlx_lm/generate.py", line 1841, in _next
self._prompt_batch.prompt(prompts)
File "/Users/myuser/.venvs/mlx/lib/python3.14/site-packages/mlx_lm/generate.py", line 1161, in prompt
mx.eval([c.state for c in self.prompt_cache])
RuntimeError: [METAL] Command buffer execution failed: Insufficient Memory (00000008:kIOGPUCommandBufferCallbackErrorOutOfMemory).
สำหรับคนที่ไม่คุ้นกับ Python เวลาเจอ Error ยาวๆ ให้เลื่อนไปดู บรรทัดสุดท้าย เสมอ ในที่นี้ [METAL] คือชิปกราฟิกของ Mac และ Insufficient Memory หมายถึงหน่วยความจำที่ GPU ขอใช้ไม่พอแล้ว
ต้นตอของปัญหามาจากบรรทัดที่บอกว่า self.prompt_cache เวลาที่เราคุยกับโมเดลแบบต่อเนื่อง ระบบจะเก็บประวัติการสนทนา (Context) ไว้ในหน่วยความจำเพื่อให้ตอบกลับได้เร็วขึ้น ยิ่งคุยยาว Context ยิ่งใหญ่จนกิน RAM หมด และทำให้โปรแกรมแครชในที่สุด
จุดนี้อธิบายอาการ “พังเป็นพักๆ” ได้หมด มันไม่ได้สุ่ม แต่มันสะสม ยิ่งงานไปได้สวยและคุยกันยาว ก็ยิ่งใกล้เพดาน เลยรู้สึกเหมือนโดนแกล้งทุกครั้งที่กำลังจะเสร็จ
วิธีแก้แบบบ้านๆ: รีสตาร์ทเซิร์ฟเวอร์

ตอนแรกผมใช้วิธีเช็คว่าเซิร์ฟเวอร์ยังตอบสนองอยู่ไหม ถ้าไม่ก็สั่ง kill process แล้วเปิดใหม่:
# เช็คสถานะ
curl -sS -m 10 http://127.0.0.1:8080/v1/models || echo "Server is down"
# ถ้าค้างก็ฆ่าทิ้งแล้วเปิดใหม่
pkill -f 'mlx_lm.server' || true
python -m mlx_lm.server --model "$MLX_MODEL" --host 127.0.0.1 --port 8080 &
การรีสตาร์ทช่วยเคลียร์ prompt_cache ให้ว่างเปล่า ทำให้รันต่อได้จริง แต่มันค่อนข้างเสียเวลาและกวนใจเวลาเรากำลังโฟกัสกับงาน — ที่สำคัญคือมันเป็นวิธีที่ต้องมีคนนั่งเฝ้าหน้าจอตลอด กว่าจะรู้ว่าค้างบางทีก็นั่งรอไปแล้วหลายนาที
จุดเปลี่ยน: ถาม Claude แล้วเลิกนั่งกดเอง

พอทนรีสตาร์ทเองไม่ไหว ผมเลยเอา Error ทั้งก้อนไปถาม Claude ว่าตกลงมันพังเพราะอะไรกันแน่ และควรแก้ที่จุดไหน คำตอบที่ได้เปลี่ยนวิธีคิดของผมไปเลย เพราะแทนที่จะบอกวิธี “กดเปิดใหม่ให้ไวขึ้น” มันชี้ให้เปลี่ยนแนวคิดจาก “พยายามไม่ให้มันพัง” มาเป็น “พังแล้วก็ลุกให้ไว” พร้อมกับเพิ่มลิมิตไม่ให้มันกิน RAM เร็วเกินไป
- จำกัดความยาวคำตอบ ด้วยการเติม flag
--max-tokens 2048เพื่อป้องกันไม่ให้โมเดลตอบยาวเกินจน RAM พุ่ง - เลือกโมเดลให้เหลือ headroom ใช้ 4-bit quantization แทนที่จะไล่หาตัวใหญ่สุดที่ “พอดีเป๊ะ” เพราะพอดีเป๊ะแปลว่าไม่มีที่เหลือให้พลาด
- เขียนสคริปต์ Auto-Restart ให้รันเซิร์ฟเวอร์แบบ loop ไปเลย ถ้าแครชก็แค่รอ 3 วินาทีแล้วเปิดขึ้นมาใหม่:
#!/usr/bin/env bash
# run-llm.sh
set -euo pipefail
: "${MLX_MODEL:?กรุณาตั้งค่าตัวแปร MLX_MODEL ก่อนรันสคริปต์}"
PORT="${PORT:-8080}"
echo "Starting MLX Server with model: $MLX_MODEL"
while true; do
python -m mlx_lm.server \
--model "$MLX_MODEL" \
--host 127.0.0.1 \
--port "$PORT" \
--max-tokens 2048 \
|| echo "[watchdog] Server crashed with exit code $?. Restarting in 3 seconds..." >&2
sleep 3
done
หลังจากใช้สคริปต์นี้ การทำงานก็ราบรื่นขึ้นมาก ต่อให้มีจังหวะที่ OOM ตัวเซิร์ฟเวอร์ก็จะเด้งกลับมาพร้อมทำงานต่อแทบจะทันที จากเดิมที่ล้มทีเสียเวลาหลายนาที เหลือแค่สามวินาทีแล้วไปต่อ จนสุดท้ายผมแทบไม่สนใจแล้วว่ามันล้มไปกี่รอบ เพราะงานไม่ได้หยุด
Deploy ขึ้น GitHub Pages
เมื่อได้ไฟล์เว็บ static ครบถ้วน ก็ถึงเวลา deploy ขึ้น GitHub Pages ผ่าน GitHub Actions:
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: ./site # ระบุ path ที่เก็บไฟล์ HTML ของโปรเจกต์
- id: deploy
uses: actions/deploy-pages@v4

ผลงานที่เสร็จแล้วเปิดดูได้จริงที่ kangana1024.github.io/test-local-llm — เว็บท่องเที่ยวทะเลพร้อมรูปการ์ตูนที่ Local LLM เขียนให้ทั้งหมด
มันมีความย้อนแย้งที่น่ารักอยู่นะ — เราเสียเวลาทั้งวันสู้กับเครื่องที่เป็นของเราเอง แล้วสุดท้ายงานที่เชื่อถือได้ที่สุดของวัน คือส่วนที่ยกให้เครื่องของคนอื่นทำ
สรุปบทเรียน
- การรัน Local LLM บน Mac ทำได้ง่ายมากด้วย
mlx_lmแต่อย่าลืมเลือกโมเดลแบบ 4-bit quantization เพื่อเซฟ RAM - OOM เป็นเรื่องปกติ เมื่อทำงานกับ context ยาวๆ การตั้งค่า
--max-tokensช่วยบรรเทาได้ - “เป็นพักๆ” มักแปลว่า “สะสม” ถ้าอาการมาตอนกำลังไปได้สวย ให้สงสัยของที่โตขึ้นเรื่อยๆ ก่อนเสมอ
- เขียนสคริปต์ Watchdog ให้เซิร์ฟเวอร์ auto-restart ดีกว่าต้องมานั่งมอนิเตอร์และกดเปิดใหม่เอง
- อ่าน Error log จากล่างขึ้นบนเสมอ บรรทัดสุดท้ายมักจะบอกสาเหตุที่แท้จริง
วินัยข้อสุดท้ายที่ติดมาจากงานนี้ ผมเคยเขียนไว้ในบทความ ตั้ง Project & DevOps ให้พร้อมรบ — หลักการเดียวกับ ${MLX_MODEL:?...} ในสคริปต์ข้างบน คือ ค่าที่ขาดไม่ได้ ต้องฟ้องตั้งแต่ก่อนเริ่ม ไม่ใช่ปล่อยให้พังกลางทาง
ขอบคุณ Claude
ขอบคุณ Claude ที่ช่วยอ่าน Error ก้อนนั้นแล้วชี้ให้เห็นว่าปัญหาไม่ได้อยู่ตรงที่ผมกำลังเพ่งอยู่ ถ้าไม่ได้คำแนะนำวันนั้น ผมคงยังนั่งกดปิด-เปิดอยู่จนถอดใจไปแล้ว งานนี้ได้จบเพราะมีคนช่วยดูอีกมุมหนึ่งจริงๆ
อ่านต่อ
- ผลงานที่เสร็จแล้ว: เว็บท่องเที่ยว test-local-llm บน GitHub Pages
- โค้ดทั้งหมดอยู่ที่ repository test-local-llm
- ตั้ง Project & DevOps ให้พร้อมรบ — วินัยเรื่องตัวแปรที่ไม่มีค่า default ให้ลอก แบบเดียวกับที่ใช้ในบทความนี้
- อัปเกรด Astro 6 → 7 บนบล็อกตัวจริง — อีกหนึ่งบันทึกลงมือทำจริงพร้อมตัวเลขก่อน-หลัง
- ดูบทความอื่นในแท็ก devops และ local-llm