2026-10-02

nar-server 구현 이후: AMD GPU 가속부터 Tev1·Nimble·Jev 실측까지

Laya·Kev·Jev 서버와 WebUI·MCP를 구현하고 AMD GPU를 붙인 뒤, Ollama의 Tev1·Nimble과 Jev를 같은 언어 판별 입력으로 비교했다. 동작 검증과 판단 품질이 왜 별개인지 실제 실패 사례로 정리한다.

앞선 글에서는 Jev·Laya·Kev를 공통 계약과 MCP로 연결하는 구조를 설명했다. 이번에는 구현계획이 실제 서버가 된 과정, AMD GPU에 모델을 올리면서 확인한 제약, 그리고 같은 질문을 여러 모델에 던져 본 결과를 정리한다.

서버를 완성하고 나서 가장 크게 달라진 질문은 이것이었다.

응답이 정상적으로 오는가? → 이 응답을 믿고 다음 작업을 진행해도 되는가?

첫 번째는 계약·운영 테스트로 확인할 수 있었다. 두 번째는 모델을 직접 비교해야 했다. 간단해 보이는 작성 언어 판별에서도 높은 확률의 오답이 반복됐다.

이 글은 9월 22–23일의 구현·GPU 검증 기록과 9월 30일의 언어 진단 원자료를 2026년 10월 2일에 다시 대조한 기록이다. 글을 쓰면서 전체 live 테스트를 재실행하지는 않았다. 이후 변경된 모델이나 런타임의 결과로 확대하지 않는다.

1. 계획에서 네 개의 실행 가능한 서버로

최초 목표는 로컬 Laya와 원격 Jev를 같은 인터페이스로 사용하는 것이었다. 여기에 Kev 4B를 추가하면서 구성이 다음과 같이 됐다.

서비스 역할 기본 포트
WebUI·MCP 게이트웨이 공통 계약, 인증, backend 선택과 장애 전환 8760
Laya English·Multilingual·Typed decisions 로컬 추론 8761
Jev 중계 TypeSafe 원격 API 연결 8762
Kev 4B 로컬 모델과 TypeSafe 호환 endpoint 8763

Python 환경은 프로젝트 루트의 .venv 하나로 관리하고, 의존성은 pyproject.toml과 uv.lock에 기록했다. 패키지 이름은 nar_server, nar_laya, nar_jev, nar_kev로 나눴다.

네 서버는 공통 POST /v1/evaluate 계약을 사용한다. 모델을 실행하기 전 적격성을 확인하는 /v1/preflight, 실제 모델·device·준비 상태를 확인하는 /v1/capabilities도 제공한다. 프로세스가 살아 있다는 /healthz와 평가 준비가 됐다는 /readyz는 구분했다.

MCP 도구는 nar_capabilities와 nar_evaluate다. Streamable HTTP와 stdio 모두 기존 HTTP backend를 사용하므로, IDE를 하나 더 연결할 때마다 모델을 새로 적재하지 않는다.

Jev는 공식 비동기 SDK 클라이언트를 재사용하고 jev-1.13.0에 고정했다. SDK 자체 retry는 꺼서 게이트웨이의 시도 예산과 중첩되지 않게 했다. API 키는 서버에서만 읽고 WebUI에 전달하지 않는다.

2. WebUI와 우선순위가 필요해진 이유

처음에는 Laya 우선, 실패하면 Jev라는 구성이었다. 그런데 모델의 품질은 네트워크 성공 여부와 달랐다. 정상 응답을 받았는데 판단이 틀리면 가용성 failover는 작동하지 않는다.

개발 중 ありがとうございます를 분류한 화면에서 Laya Multilingual이 English를 고른 사례가 있었다. 다만 당시 Jev 화면은 Score, Laya 화면은 Choice였고 후보 구성도 달랐다. 문제를 발견하는 계기는 됐지만, 그 화면 두 장을 공정한 모델 비교로 취급하지는 않았다.

그래서 기본 우선순위와 요청별 선택을 분리했다.

NAR_BACKEND_PRIORITY=laya,kev,jev

이는 코드의 기본 순서다. Jev를 먼저 사용하고 싶다면 jev,laya,kev로 바꿀 수 있고, 요청의 routing.primary로 해당 요청만 우선 backend를 지정할 수도 있다.

WebUI에서는 서버 기본값·Laya 우선·Kev 우선·Jev 우선을 선택한다. 이 선택은 현재 탭의 요청에 적용되고 다른 MCP 클라이언트의 기본값을 바꾸지 않는다. MCP에서도 같은 요청 필드로 선택한다.

화면은 질문, primitive 설정, 평가 대상 state, 결과를 중심으로 만들었다. Choice 후보의 사용 체크박스, Score의 순서 있는 단계, Noul의 선택적 참·거짓 기준을 편집하고 템플릿으로 저장한다. 템플릿은 브라우저 localStorage에 보관하며 JSON 가져오기·내보내기를 지원한다. 평가 원문과 인증 토큰은 템플릿에 넣지 않는다.

인증은 none·Bearer·OAuth를 지원한다. OAuth의 PKCE, discovery, 동적 클라이언트 등록, refresh 회전과 폐기를 확인했다. 다만 현재 방식은 단일 소유자 승인용이다. 외부 도메인 배포나 다중 사용자 권한 시스템까지 완성했다는 뜻은 아니다.

3. 라우터에서 유지한 경계

backend를 늘리면서도 다음 동작은 유지했다.

  • 원격 전송 정책: 서버와 요청이 모두 허용할 때만 Jev를 호출한다.
  • 요청 전체의 일관성: 한 요청의 질문들을 서로 다른 backend 결과로 섞지 않는다.
  • 입력 보존: state·질문·후보를 잘라서 로컬 모델에 억지로 맞추지 않는다.
  • 시도 예산: backend별 한 번, 세 backend를 허용하면 최대 세 번이며 하나의 전체 deadline을 공유한다.
  • 실행 슬롯: timeout이 발생해도 실제 추론이 끝날 때까지 슬롯을 점유하고 늦은 결과를 버린다.

낮은 confidence는 정상적인 판단 결과다. 연결 장애처럼 처리하지 않는다. 품질을 개선하려고 다른 모델을 호출하는 정책과, 장애 때문에 대체 backend를 호출하는 정책은 별도로 설계해야 한다.

4. AMD GPU 가속: Laya와 Kev는 서로 다른 경로였다

실행 환경은 Windows와 Radeon RX 6900 XT였다. GPU를 사용한다는 표시만으로 완료 처리하지 않고, 실제 DirectML kernel 실행과 CPU 대비 출력 차이를 확인했다.

Laya: encoder와 head를 함께 ONNX로 변환

English·Multilingual·Typed decisions 세 모델을 FP32·opset 17로 변환했다. encoder뿐 아니라 decision/action head와 temperature 보정 이후의 출력도 비교했다.

검증 기준은 확률 차이 0.001 이하, Score 차이 0.01 이하, 명확한 최댓값의 Choice 일치였다. 첫 Multilingual 변환은 최대 확률 차이 0.0011로 기준을 넘었다. vendor metacommand를 끄고 재검증한 산출물을 사용했다.

최종 합성 샘플에서는 제공자 응답 정밀도인 소수 네 자리로 비교한 확률·Score 최대 차이가 0이었다. 내부 tensor 전체가 bitwise로 같다는 뜻도, 모든 길이에서 같은 결과를 보장한다는 뜻도 아니다.

짧은 입력에서는 GPU가 항상 더 빠르지 않았다. 모델 크기만 보고 GPU 가속 효과를 단정할 수 없었다.

Kev: 전체 GPU 적재 대신 MLP 일부를 옮겼다

Kev는 Qwen3.5 계열의 모델과 pointer head를 사용한다. 이 환경에서는 32개 MLP 중 24개를 DirectML FP32로 옮기고, 나머지 연산은 PyTorch CPU FP32에 남겼다. 실제 device는 directml+cpu로 표시한다.

128-token chunk는 token별로 독립인 MLP 연산의 임시 버퍼를 줄이는 설정이다. attention 문맥이나 입력 내용을 절단하는 기능이 아니다.

같은 일본어 문구를 warmup 뒤 각 5회 반복한 당시 측정은 다음과 같다.

Kev 실행 경로 모델 추론 중앙값
CPU FP32 938.80ms
DirectML + CPU FP32 580.33ms

이 한 입력에서는 약 1.62배 빨랐고 지연은 약 38.2% 줄었다. Laya도 동시에 적재된 환경이었다. 이 수치를 뒤에서 소개하는 Ollama의 HTTP 왕복 시간과 직접 비교하지는 않는다.

GPU로 옮긴 가중치의 산술 크기는 약 6.33GiB였다. 전체 시스템의 peak VRAM은 측정하지 않았다. Laya를 내리면 공간이 늘겠지만, 그것만으로 Kev 전체 GPU 실행이 구현되거나 검증되는 것은 아니다. FP16·양자화·MLX도 각각 별도 실행 경로와 품질 검증이 필요하다.

관련 구현의 기반은 Kev 고정 소스와 ONNX Runtime DirectML이다.

5. 구현 검증이 끝났다고 품질 검증도 끝난 것은 아니었다

개발 기록에는 Kev GPU 추가 후 모델 없는 테스트 115개 통과가 남아 있다. 계약, 원격 금지, 큐, 취소, 잘못된 출력, 변환 산출물 검증 같은 검사다. 그보다 앞선 통합 단계에서는 Laya CPU 검사 18개와 전체 live 검사 16개도 통과했다.

이들은 서로 다른 개발 시점의 기록이다. 하나의 최신 실행에서 전부 다시 통과한 숫자로 합산하지 않는다.

실제 WebUI 템플릿 동작, MCP HTTP·stdio의 initialize/list/call, TypeSafe SDK 연결도 확인했다. 그러나 테스트가 보장하는 것은 정해진 입출력과 실행 경로가 동작한다는 사실이다. 질문의 답이 맞는지는 다른 평가가 필요했다.

6. 직접 가속을 붙이는 동안 Ollama에도 System One이 들어왔다

Ollama 0.35 계열에서 /v1/systemone으로 decision model을 호출할 수 있게 됐다. Tev1과 Nimble은 state와 questions를 받고, 후보별 분포와 선택값을 반환한다.

이 변화로 로컬 모델 실행을 기존 런타임에 맡길 선택지가 생겼다. 그래도 공통 계약, 원격 전송 정책, MCP, 템플릿, 응답 검증은 게이트웨이에 남길 수 있다.

중요한 현재 상태는 다음과 같다.

nar-server에 통합된 backend는 Laya·Kev·Jev다. 아래 Tev1·Nimble 평가는 별도 CLI 스크립트에서 Ollama를 직접 호출한 결과이며, Ollama를 nar-server backend로 추가한 작업은 아직 아니다.

9월 30일에 테스트한 모델은 다음과 같다.

모델 호출한 ID 당시 버전 식별
Tev1 4B tev1:4b Q8_0, digest 앞 12자리 cef45ef93cf6
Nimble 9B nimble:latest Q8_0, digest 앞 12자리 24e550a16a70
Jev jev-1.13.0 97개 응답 모두 같은 모델 ID 확인

Ollama는 0.35.0, Jev SDK는 0.7.1이었다. Nimble은 alias로 호출했지만 당시 digest를 원자료에 남겼다. 10월 2일 확인한 Ollama 라이브러리에서는 태그가 이미 바뀐 경우가 있었다. 지금 같은 이름을 pull한 결과에 아래 점수를 그대로 적용하면 안 된다.

7. “이 문장은 어느 언어인가?” 40개 입력을 준비했다

공개 종합 벤치마크 대신, 실제로 궁금했던 언어 판별의 실패 양상을 보기 위한 작은 합성 진단을 만들었다. 추론 전에 입력과 기대 정답을 정하고 세 모델에 같은 내용을 보냈다.

범주 수 확인하려는 것
일반 문장 20 한국어·일본어·영어·중국어와 그 밖의 언어
로마자 표기 4 한국어·일본어를 라틴 문자로 쓴 경우
혼합 언어 4 두 언어의 실질적인 절이 함께 있는 경우
언어에 관한 주장·인용 4 문장 내용과 실제 작성 언어를 구분하는가
단독으로 애매한 입력 8 한자·짧은 단어·숫자·이모지에서 판단을 보류하는가

두 가지 질문 설정을 사용했다.

기본형은 instructions를 Language로 두고, Korean·Japanese·English·Other 네 후보의 설명을 null로 보냈다.

명시형은 Chinese·Mixed·Undetermined를 추가하고 후보 설명을 붙였다. 지시문은 다음과 같았다.

Identify the language actually used in the supplied text, not its topic or claims about language. Recognize romanized Korean and Japanese. Ignore a short foreign quotation or loanword inside an otherwise single-language sentence. Choose Mixed when substantial clauses use multiple languages. Choose Undetermined when the text alone cannot distinguish a language (shared Han characters, shared short words, numbers, or emoji).

“본문이 주장하는 언어 대신 실제 작성 언어를 판별하라”, “짧은 외국어 인용만으로 혼합 언어로 보지 말라”, “근거가 부족하면 보류하라”는 규칙이다.

두 설정은 지시문뿐 아니라 후보 수와 설명도 달라진다. 결과 차이를 프롬프트 문장 하나의 효과로 단정하지 않았다.

기본형에는 혼합 언어 후보가 없으므로 그 4건은 채점하지 않았다. 애매한 8건도 두 설정 모두 단일 정답 정확도에서 제외했다. 따라서 기본형은 28건, 명시형은 32건이 분모다.

모델별로 본 평가 80회, 준비 요청 1회, 후보 순서 추가 검사 16회, 총 97회 요청했다. 세 모델 합계 291회다. Laya와 Kev는 이 40개 입력 비교에 포함하지 않았다. 초기 단일 문구 관찰과 이번 paired 비교를 섞지 않기 위해서다.

8. 일반 문장보다 “문장 속 주장”이 문제였다

설정 범주 Tev1 4B Nimble 9B Jev 1.13.0
기본형 일반 문장 19/20 20/20 20/20
기본형 로마자 표기 4/4 4/4 4/4
기본형 주장·인용 1/4 1/4 2/4
명시형 일반 문장 19/20 18/20 20/20
명시형 로마자 표기 4/4 4/4 4/4
명시형 혼합 언어 4/4 2/4 4/4
명시형 주장·인용 1/4 1/4 4/4

명시형 합계는 Tev1 28/32, Nimble 25/32, Jev 32/32였다. 기본형은 각각 24/28, 25/28, 26/28이었다.

핵심 차이는 아래 사례에서 드러났다. 괄호의 값은 선택 후보의 확률이며 정답률이나 서로 보정된 confidence가 아니다.

입력 기대 판단 Tev1 명시형 Nimble 명시형 Jev 명시형
This sentence is written in Japanese. English Japanese 95.92% Japanese 98.70% English 98%
이 문장은 영어로 작성되었습니다. Korean English 96.02% English 98.89% Korean 98%
The Japanese word "ありがとう" means thank you. English Japanese 82.92% Japanese 99.61% English 75%

마지막 사례는 짧은 외국어 인용을 제외한다는 평가 규칙에 따른 정답이다.

관찰상 Tev1과 Nimble은 실제 작성 언어보다 본문 속 언어 주장이나 눈에 띄는 인용에 끌리는 양상을 보였다. “실제 언어를 보라”는 지시를 넣어도 이 사례들의 오답은 유지됐다. 이것만으로 모델 내부 원인을 규명한 것은 아니다.

예상 밖의 일반 문장 오류도 있었다. 명시형에서 Tev1은 스페인어 Muchas gracias por tu ayuda.를 Korean 72.52%로 골랐다. Nimble은 중국어 谢谢你的帮助。를 Korean 58.31%, 러시아어 Спасибо за вашу помощь.를 Korean 69.13%로 골랐다. 이 입력들은 기본형에서는 맞혔다.

모델을 크게 바꾸거나 설명을 늘린다고 항상 개선되지는 않았다.

Jev는 명시형에서 채점 대상 32개를 모두 맞혔다. 하지만 기본형에서는 주장·인용 4개 중 2개를 틀렸다. Jev도 질문을 대충 써도 되는 모델이라는 결론은 나오지 않았다.

9. 애매한 입력에서는 보류와 후보 순서를 봤다

애매한 입력은 東京, 先生, 愛, OK, www, 12345, 🙂👍, gift였다. 문맥 없이 하나의 작성 언어를 확정하기 어렵거나, 언어 정보 자체가 부족한 입력들이다.

명시형에서 Undetermined를 선택한 횟수는 다음과 같았다.

모델 8건 중 판단 보류
Tev1 4B 2건
Nimble 9B 1건
Jev 1.13.0 5건

이것은 정확도 순위가 아니라 보류 동작의 관찰이다. Jev도 先生은 Chinese, OK와 gift는 English를 선택했다.

같은 8개 사례를 골라 원래 후보 순서와 역순으로 추가 호출했다. Tev1은 8건 모두 선택값을 유지했다. Nimble은 스페인어 문장이 Other 99.12% → Korean 96.65%로 바뀌었다. Jev는 東京와 先生에서 언어 선택과 보류가 바뀌었다.

같은 순서의 반복에서도 일부 확률은 달랐다. 각각 한 번의 역순 검사이므로 차이를 순서 효과만으로 단정하거나 전체 모델의 안정성 수치로 일반화하지 않았다. 다만 후보 순서도 실제 검증 조건에 넣어야 한다는 근거는 됐다.

10. 속도와 메모리를 함께 보면

아래는 준비 요청을 제외한 본 평가의 요청 왕복 시간이다.

모델 기본형 p50 / p95 명시형 p50 / p95
Tev1 4B 151 / 161ms 201 / 208ms
Nimble 9B 220 / 228ms 325 / 327ms
Jev 1.13.0 195 / 265ms 193 / 233ms

Jev는 네트워크와 SDK 호출 시간이 포함된 원격 요청이고, 로컬 모델은 Ollama HTTP 요청이다. 캐시·GPU 클럭·시스템 부하·네트워크를 통제한 순수 추론 성능 비교는 아니다.

Ollama는 두 로컬 모델 모두 100% GPU로 표시했다. 당시 /api/ps가 보고한 GPU 적재량은 Tev1 약 4.67GB, Nimble 약 8.98GB였다. 이는 보고된 적재량이지 peak VRAM 측정값은 아니다. Kev의 산술 가중치 크기와도 측정 정의가 다르다.

이번 언어 진단에서는 Nimble이 더 많은 메모리를 쓰면서 핵심 오류를 해결하지 못했다. 그렇다고 다른 정책 판단이나 분류에서도 Tev1이 항상 더 낫다는 뜻은 아니다.

97회 Jev 응답의 usage 합계는 입력 40,479토큰, 출력 5,785토큰이었다. 실제 결제 금액은 조회하지 않았다. 제공자의 output_tokens 숫자를 자유로운 문장 생성량과 동일하게 해석하지도 않았다.

11. 그래서 영어 context만 사용하면 될까?

영어로만 제한하는 것으로는 이번 오류를 해결할 수 없다. This sentence is written in Japanese. 자체가 이미 완전한 영어 문장이기 때문이다.

반대로 Jev는 같은 설정에서 한국어·일본어·로마자·혼합 입력을 처리했다. 이번에 얻은 결론은 “다국어는 포기해야 한다”가 아니라 모델과 질문 설정을 함께 평가해야 한다는 것이다.

언어 판별이 제품의 핵심 기능이라면 전용 언어 감지기를 비교 대상으로 넣을 수 있다. 좁은 정책 판단 모델을 쓴다면 정상 사례뿐 아니라 다음과 같은 입력을 함께 준비해야 한다.

  • 본문이 실제 사실과 다른 주장을 하는 경우.
  • 짧은 외국어 인용이나 로마자 표기가 들어간 경우.
  • 후보 어느 것에도 맞지 않거나, 여러 후보가 동시에 가능한 경우.
  • 정보가 부족해서 보류해야 하는 경우.
  • 후보 순서나 설명을 바꾸면 선택이 흔들리는 경우.

특히 높은 확률의 오답이 이미 있었으므로, confidence 임계값 하나로 자동 처리 여부를 정하면 이 실패들을 놓칠 수 있다.

12. 다음 backend는 실행 환경보다 평가 결과를 보고 붙인다

OpenAI도 9월 29일 DevDay에서 Luna 기반 Decisions API를 발표했다. 텍스트·이미지 context와 유한한 답변을 가진 질문으로 분류·라우팅·행동 선택을 수행한다는 설명이다. 발표 당시 상태는 제한된 프리뷰였다. 공식 발표

이 글의 구현 범위에는 Decisions API 연동이나 실측이 없다. Jev의 primitive·확률 출력과 어떤 차이가 있는지, 기존 계약에 어떻게 대응시킬 수 있는지부터 확인해야 한다.

현재 남은 작업도 구체적이다. Ollama를 공통 backend로 붙이는 일, Laya·Kev까지 같은 진단에 넣는 일, 언어 감지를 넘어 실제 사용할 업무의 정답과 보류 기준을 만드는 일이다. 긴 입력, 지속 부하, peak VRAM, calibration도 별도로 남아 있다.

직접 서버와 GPU 경로를 구현하는 동안 범용 런타임에도 기능이 추가됐다. 그 변화는 모델 실행부를 교체할 선택지를 늘려 줬다. 구현 과정에서 남은 더 중요한 자산은 입력을 보존하는 계약, 출처를 확인할 수 있는 응답, 원격 전송 경계, 그리고 같은 질문으로 모델을 비교할 수 있는 검증 절차였다.

API 호환성은 연결할 수 있는지를 말해 준다. 어떤 판단을 맡겨도 되는지는 실제 입력과 실패 사례가 말해 준다.