SONUB
KO English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

알림을 켜는 방법

이 브라우저 또는 주소에서는 알림을 사용할 수 없습니다. 지원되는 브라우저로 보안 연결된 SONUB 사이트에 접속해 주세요.

알림이 차단되어 있으면 브라우저 설정에서 허용해 주세요.

  1. 주소 표시줄 옆의 사이트 설정 또는 브라우저의 사이트 설정을 열고, 이 사이트의 알림을 허용으로 바꾸세요.
  2. 기기 설정에서 브라우저 알림을 허용하고 방해 금지 또는 집중 모드를 확인하세요.
  3. SONUB으로 돌아와 페이지를 새로고침하세요. 알림이 켜지면 벨은 사라집니다.

iPhone 또는 iPad에서는 SONUB을 홈 화면에 추가하고 거기에서 연 뒤 알림을 허용하세요.

로그인 로그아웃 화면 테마 기본 테마 다크 테마 핑크 테마 채팅 1:1 채팅 그룹 채팅 게시판 공지사항 뉴스 게시판 건의 게시판 자기소개 게시판 메인 메뉴

자, 이제 여러분들만의 인공지능을 만들어 봅시다.

한 을 실제 로 실행하는 를 만듭니다. runner.py 를 백그라운드로 띄우고 로 와 /generate 를 불러 보세요.

단어 블록이 한 줄로 놓여 있고 끝에 빈칸이 있다. 빈칸 위에 높이가 다른 막대를 단 후보 블록 세 개가 있고, 그중 하나가 골라져 줄 끝에 붙은 뒤 화살표가 다시 처음으로 돌아가 다음 칸을 고른다.
8

Runner·API

는 체크포인트를 한 번 읽고 요청을 받아 generate를 합니다. 는 요청을 하나씩 처리합니다. 는 실행 상태를, /generate는 생성 문장을 돌려줍니다. 공개 서비스용이 아닌 수업용 입니다.

은 자신의 주소입니다. 아래 은 반드시 이 에서 실행합니다. 주소창의 는 다른 컴퓨터를 가리킵니다. 과 &로 뒤에서 실행할 수 있지만 이 탭이 살아 있는 동안뿐입니다. 새로 고침하거나 닫으면 종료됩니다. 여기서는 가 필요 없습니다.

왜 서버로 만드나 — 한 번 읽고 여러 번 쓴다

generate.py 를 으로 실행하면 실행할 때마다 체크포인트를 읽고 을 다시 만듭니다. 질문이 많으면 그때마다 같은 준비를 되풀이합니다. Runner 는 모델을 한 번만 읽어 에 두고, 요청이 올 때마다 generate 만 부릅니다.

서버로 나누면 역할도 갈립니다.

  • Runner(서버): 모델을 들고 계산만 합니다.
  • (): 사람과 말을 주고받는 화면만 맡습니다.
  • 둘 사이의 약속(): 어느 주소로 어떤 JSON 을 보내면 어떤 JSON 이 돌아오는지.

약속만 지키면 클라이언트는 무엇이든 될 수 있습니다. 터미널 XChat 대신 웹 페이지·휴대폰 앱·다른 서버가 같은 Runner 를 부를 수 있습니다. 한 를 읽어 계산을 맡는 이런 을 이라고도 합니다. Runner 는 아주 작은 런타임이고, 실제로는 ·· 같은 런타임이 같은 일을 빠르게 합니다.

HTTP 기초 — 요청과 응답의 모양

는 클라이언트가 요청을 보내고 서버가 응답을 돌려주는 약속입니다.

  • 요청: 메서드(GET 은 가져오기, POST 는 보내기)·(/health, /generate)·헤더(: application/json 처럼 본문의 종류)·본문(POST 의 JSON).
  • 응답: ·헤더·본문.
  • 상태 코드: 성공, 400 잘못된 요청(클라이언트 탓), 없는 경로, 500 서버 오류(서버 탓), 503 아직 준비 안 됨.

curl 로 요청을 직접 만들 수 있습니다.

  • curl -s 주소: GET 요청을 보내고 본문만 조용히 보여 줍니다.
  • -H 'Content-Type: application/json': 헤더를 붙입니다.
  • -d '{…}': 본문을 붙입니다. 본문이 있으면 curl 이 POST 로 보냅니다.
  • -i: 응답의 상태 줄과 헤더까지 보여 줍니다. curl -i http://127.0.0.1:8765/health 를 쳐 보세요. HTTP/1.0 200 OK, Content-Type, Content-Length 줄이 보입니다.

Runner 의 API — 주소·필드·제한

Runner 는 두 주소만 엽니다.

  • GET /health: 살아 있는지 확인합니다. 응답은 {"status": "ok", "model": ""}.
  • POST /generate: 글을 생성합니다. 응답은 {"text": "생성한 글", "model": "XSLM"}.
  • 그 밖의 주소: 404 와 {"error": "not found"}.

/generate 본문의 필드입니다.

  • (필수): 문자열, 1~512글자.
  • : 정수, 1~64(max_tokens). 없으면 24.
  • : 0 이상의 숫자. 없으면 0.8.
  • : 0 이상의 정수. 없으면 8.
  • : 정수. 없으면 7.

잘못된 요청에는 서버가 죽지 않고 400 과 {"error": "invalid request"} 로 답합니다. 본문이 4,096를 넘거나, JSON 이 아니거나, 필드 형식이 틀리거나, prompt 가 없으면 모두 400 입니다. 제한을 두는 까닭은 서버를 지키기 위해서입니다. tokens 를 100만으로 보내는 요청 하나가 서버를 몇 시간 붙잡아 둘 수 있습니다. 공개 서버라면 이런 검사가 반드시 필요합니다.

127.0.0.1 과 포트 — 누가 접속할 수 있나

Runner 는 127.0.0.1 의 8765번 에서 요청을 기다립니다.

  • 127.0.0.1(localhost): 루프백 주소로 "이 컴퓨터 자신"을 뜻합니다. 같은 컴퓨터 안의 프로그램만 접속할 수 있습니다.
  • 포트: 한 컴퓨터에서 여러 서버를 구별하는 번호입니다. 8765 는 settings.json 의 port 입니다.
  • 0.0.0.0: 모든 에서 받겠다는 뜻입니다. 다른 컴퓨터에서 접속하게 하려면 이렇게 열지만, 없이 인터넷에 열면 누구나 내 서버를 쓸 수 있으므로 조심합니다.

여기서 "이 컴퓨터"는 브라우저 안의 가상 리눅스입니다. 그래서 curl 과 XChat 은 반드시 오른쪽 터미널 안에서 실행합니다. 의 브라우저 주소창에 localhost:8765 를 치면 가상 리눅스가 아니라 내 컴퓨터를 가리켜 접속되지 않습니다.

같은 포트를 두 프로그램이 동시에 쓸 수는 없습니다. Runner 가 이미 켜져 있는데 또 켜면 OSError: 가 납니다.

백그라운드 실행 — &·nohup·PID·kill

Runner 는 끝나지 않고 계속 요청을 기다리는 프로그램입니다. 그냥 실행하면 터미널을 차지해 다른 명령을 칠 수 없으므로 뒤에서() 실행합니다.

  • &: 명령 끝에 붙이면 뒤에서 실행하고 프롬프트를 바로 돌려줍니다.
  • nohup: 터미널이 닫혀도 프로그램이 끝나지 않게 합니다.
  • -u: 출력을 모아 두지 않고 바로 씁니다. 로그가 늦게 찍히지 않습니다.
  • > runner.log : 보통 출력과 오류 출력을 모두 runner.log 파일에 씁니다.
  • $!: 방금 뒤에서 시작한 프로그램의 (PID)입니다. $! > runner.pid 로 저장해 둡니다.
  • "$(cat runner.pid)": 저장한 번호의 프로그램에 끝내라는 신호를 보냅니다.
  • ps 나 jobs: 지금 도는 프로그램을 봅니다.

실제 서버는 컴퓨터가 다시 켜져도, 프로그램이 죽어도 자동으로 다시 켜야 합니다. 는 systemd 서비스(Restart=always)로, 는 launchd 로 이 일을 합니다. 이 가상 리눅스의 Runner 는 이 탭이 열려 있는 동안만 돕니다.

동시 요청 — 하나씩 처리하기와 줄 세우기

XSLM 의 HTTPServer 는 요청을 한 번에 하나씩 처리합니다. 생성이 끝나기 전에 들어온 요청은 앞 요청이 끝날 때까지 기다립니다. 수업용으로는 단순하고 안전합니다.

여러 사람이 쓰는 서버는 다르게 합니다.

  • ThreadingHTTPServer: 요청마다 스레드를 따로 만들어 동시에 받습니다. 에 있습니다.
  • FastAPI + uvicorn: 로 많은 연결을 받고, 실제 생성은 Semaphore(1)로 한 번에 하나씩 줄을 세웁니다. 메모리는 하나뿐이라 동시에 여러 생성을 돌리면 메모리가 넘칠 수 있기 때문입니다.
  • 배치 처리: 짧은 시간(예: 100ms) 동안 들어온 요청을 묶어 한 번에 계산하면 GPU 를 훨씬 효율적으로 씁니다. 큰 서비스의 핵심 기술입니다.

FastAPI 는 요청 형식 검사(pydantic)와 /docs 의 API 문서(Swagger)를 자동으로 만들어 줍니다. XSLM 은 같은 검사를 runner.py 에 손으로 씁니다.

실제 서비스로 가려면 — 속도·크기·안전

Runner 를 실제 서비스로 키우려면 이런 것들을 더합니다.

  • 빠른 런타임: llama.cpp(·애플 칩에 강함), ONNX Runtime(여러 장치), vLLM(GPU 서버)처럼 계산을 최적화한 엔진으로 가중치를 실행합니다.
  • : 가중치를 32·16비트 실수 대신 8·4비트 정수로 저장해 메모리와 시간을 아낍니다. 8비트는 256단계로 숫자를 나타내고, 정수 × 크기 값으로 원래 숫자에 가깝게 되돌립니다. 정확도는 조금 줄어듭니다.
  • : 앞 토큰의 key·value 를 저장해 토큰마다 전체를 다시 계산하지 않습니다.
  • : 토큰이 나올 때마다 바로 보내 사용자가 기다리지 않게 합니다.
  • 안전: 인증(), 요청 횟수 제한, 입력 크기 제한, 로그와 모니터링, HTTPS.
  • 메모리 계산: 실행 메모리 = 가중치 + KV 캐시 + 입니다. 모델을 4비트로 줄여도 실제로는 5~ 가 듭니다.

직접 타이핑하며 실습하기

runner.py를 작성하고 명령대로 시작하세요. 바로 다음 줄에서 프로세스 번호를 저장합니다. runner.log를 확인한 뒤 /health와 /generate를 요청합니다. 첫 확인이 너무 빠르면 시작 문구가 나올 때까지 기다렸다 다시 요청하세요.

cd ~/xslm
micro runner.py
nohup python3 -u runner.py > runner.log 2>&1 &
echo $! > runner.pid
cat runner.log
curl -s http://127.0.0.1:8765/health
curl -s http://127.0.0.1:8765/generate -H 'Content-Type: application/json' -d '{"prompt":"Q: hi\nA: ","tokens":8,"temperature":0.8}'
터미널에서 실습하기
runner.py직접 작성할 전체 소스

요청마다 모델을 새로 만들지 않습니다. 본문을 검사하고 문장을 생성한 뒤 JSON으로 응답합니다. 가상 리눅스의 loopback에 연결하여 이 리눅스 안에서만 씁니다.

import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from generate import load, generate


def main():
    model, tokenizer, cfg = load()

    class Handler(BaseHTTPRequestHandler):
        def reply(self, status, body):
            data = json.dumps(body, ensure_ascii=False).encode('utf-8')
            self.send_response(status)
            self.send_header('Content-Type', 'application/json; charset=utf-8')
            self.send_header('Content-Length', str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def do_GET(self):
            if self.path == '/health':
                self.reply(200, {'status': 'ok', 'model': 'XSLM'})
            else:
                self.reply(404, {'error': 'not found'})

        def do_POST(self):
            if self.path != '/generate':
                return self.reply(404, {'error': 'not found'})
            try:
                length = int(self.headers.get('Content-Length', '0'))
                if not 0 < length <= 4096:
                    raise ValueError('request size')
                data = json.loads(self.rfile.read(length))
                prompt = data['prompt']
                tokens = data.get('tokens', cfg['tokens'])
                temperature = data.get('temperature', 0.8)
                top_k = data.get('top_k', 8)
                seed = data.get('seed', cfg['seed'])
                if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
                    raise ValueError('prompt')
                if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
                    raise ValueError('tokens')
                if type(top_k) is not int or top_k < 0 or type(seed) is not int:
                    raise ValueError('top_k/seed')
                if type(temperature) not in (int, float):
                    raise ValueError('temperature')
                result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
                self.reply(200, {'text': result, 'model': 'XSLM'})
            except (ValueError, KeyError, TypeError):
                self.reply(400, {'error': 'invalid request'})

    address = ('127.0.0.1', cfg['port'])
    print('XSLM Runner:', address, flush=True)
    HTTPServer(address, Handler).serve_forever()


if __name__ == '__main__':
    main()

코드 한 줄씩 뜯어보기

runner.py 모델을 한 번만 읽기

def main():
    model, tokenizer, cfg = load()

main 이 시작할 때 load() 로 모델··설정을 한 번 읽습니다. 아래 는 main 안에 정의되어 바깥의 model·tokenizer·cfg 를 그대로 씁니다(클로저). 요청마다 모델을 다시 만들지 않습니다.

runner.py reply — 응답 보내기

    class Handler(BaseHTTPRequestHandler):
        def reply(self, status, body):
            data = json.dumps(body, ensure_ascii=False).encode('utf-8')
            self.send_response(status)
            self.send_header('Content-Type', 'application/json; charset=utf-8')
            self.send_header('Content-Length', str(len(data)))
            self.end_headers()
            self.wfile.write(data)

Handler 는 BaseHTTPRequestHandler 를 이어받아 요청 하나를 맡습니다. reply 는 사전을 JSON 으로 바꿔 UTF-8 바이트로 만들고(ensure_ascii=False 라 한글도 그대로), 상태 코드 → 헤더(Content-Type·Content-Length) → 빈 줄(end_headers) → 본문 순서로 보냅니다. Content-Length 는 글자 수가 아니라 바이트 수라서 인코딩한 의 길이를 씁니다.

runner.py GET /health

        def do_GET(self):
            if self.path == '/health':
                self.reply(200, {'status': 'ok', 'model': 'XSLM'})
            else:
                self.reply(404, {'error': 'not found'})

GET 요청이 오면 do_GET 이 불립니다. self.path 가 /health 면 200 과 상태를, 아니면 404 를 보냅니다. 상태 확인 주소는 모델 계산 없이 바로 답하므로 서버가 살아 있는지 가볍게 확인하는 데 씁니다.

runner.py POST /generate — 본문 읽고 검사하기

        def do_POST(self):
            if self.path != '/generate':
                return self.reply(404, {'error': 'not found'})
            try:
                length = int(self.headers.get('Content-Length', '0'))
                if not 0 < length <= 4096:
                    raise ValueError('request size')
                data = json.loads(self.rfile.read(length))
                prompt = data['prompt']
                tokens = data.get('tokens', cfg['tokens'])
                temperature = data.get('temperature', 0.8)
                top_k = data.get('top_k', 8)
                seed = data.get('seed', cfg['seed'])
                if not isinstance(prompt, str) or not 0 < len(prompt) <= 512:
                    raise ValueError('prompt')
                if type(tokens) is not int or not 1 <= tokens <= cfg['max_tokens']:
                    raise ValueError('tokens')
                if type(top_k) is not int or top_k < 0 or type(seed) is not int:
                    raise ValueError('top_k/seed')
                if type(temperature) not in (int, float):
                    raise ValueError('temperature')

경로가 /generate 가 아니면 404 입니다. 그다음 try 안에서 하나씩 검사하고, 어느 하나라도 틀리면 를 일으킵니다.

  • Content-Length 헤더로 본문 길이를 알고, 1~4,096바이트가 아니면 거절합니다. 그 길이만큼만 rfile 에서 읽습니다.
  • json.loads 로 본문을 사전으로 바꿉니다. JSON 이 아니면 ValueError 입니다.
  • prompt 는 반드시 있어야 하고(없으면 ), 나머지는 get 으로 기본값을 줍니다.
  • type(x) is not int 로 검사하는 까닭은 isinstance(True, int) 가 참이기 때문입니다. true 를 토큰 수로 보내는 요청을 막습니다.
  • temperature 는 정수와 실수를 모두 받고, 음수· 은 generate 가 다시 거절합니다.

runner.py 생성하고 답하기

                result = generate(model, tokenizer, prompt, tokens, temperature, top_k, seed)
                self.reply(200, {'text': result, 'model': 'XSLM'})
            except (ValueError, KeyError, TypeError):
                self.reply(400, {'error': 'invalid request'})

검사를 통과하면 generate 를 불러 200 과 {"text": …} 로 답합니다. ValueError·KeyError·TypeError 는 모두 잘못된 요청이므로 400 으로 답하고 서버는 계속 돕니다. 오류마다 서버가 죽으면 요청 하나로 서비스 전체가 멈추기 때문입니다.

runner.py 127.0.0.1 에서 기다리기

    address = ('127.0.0.1', cfg['port'])
    print('XSLM Runner:', address, flush=True)
    HTTPServer(address, Handler).serve_forever()

주소는 127.0.0.1 과 설정의 포트입니다. 시작 문구를 =True 로 바로 찍어 runner.log 에서 켜졌는지 알 수 있게 합니다. HTTPServer(…).serve_forever() 는 요청을 하나씩 받아 Handler 에 넘기기를 끝없이 되풀이합니다. kill 이나 로 멈춥니다.

여기까지 이해했나요?

상태 확인은 status ok, 생성 요청은 text가 들어 있는 JSON이어야 합니다. 빈 JSON을 보내면 서버가 죽지 않고 400을 반환하나요? 응답은 정해 놓은 문장이 아니라 에서 나와야 합니다.

막혔을 때 확인하기

라면 runner.log와 체크포인트 파일을 확인하세요. Address already in use라면 기존 Runner를 쓰거나 저장한 PID로 종료한 뒤 다시 시작합니다. 새로 학습했다면 Runner를 다시 시작해야 바뀐 가중치를 읽습니다.

자주 묻는 질문

브라우저 주소창에서 localhost:8765/health 를 열면 왜 안 되나요?

Runner 는 브라우저 안의 가상 리눅스에서 돌고, 그 리눅스의 127.0.0.1 에서만 기다립니다. 주소창의 localhost 는 내 진짜 컴퓨터라서 거기에는 Runner 가 없습니다. curl 은 오른쪽 터미널에서 실행하세요.

Address already in use 가 나요.

8765번 포트를 이미 다른 Runner 가 쓰고 있습니다. 켜진 Runner 를 그대로 쓰거나, kill "$(cat runner.pid)" 로 끈 뒤 다시 켭니다. PID 파일이 없으면 ps 로 python3 runner.py 의 번호를 찾아 kill 합니다.

학습을 더 했는데 Runner 의 답이 그대로예요.

Runner 는 켤 때 checkpoints/model.json 을 한 번만 읽어 메모리에 둡니다. 파일이 바뀌어도 모릅니다. 끄고 다시 켜야 새 가중치를 씁니다. 실제 서비스는 새 모델을 옆에 띄운 뒤 요청을 옮기는 방식으로 멈추지 않고 바꿉니다.

가이드라인

인공지능

에이전트 툴

바이브코딩

AI 활용

AI 자격증

화상 수업

IT 기초

채팅

게시판

만들기

소통

더보기

용어 사전

모르는 말을 골라 보세요. 쉬운 설명과 관련 용어를 볼 수 있어요.

찾는 용어가 아직 없어요.

용어를 고르면 설명이 보여요.