[2단계] 모델 추론과 성능 최적화 - 100-hours-a-week/KTB4-3rd-wiki GitHub Wiki
[2단계] 모델 추론과 성능 최적화
기능 3의 성능 수치는 이전 설계·실행 조건에서 측정한 값이다. 지금 설계에 그대로 쓰지 않고, 기준 부하로 다시 측정한다.
기능 정리
- 기능 1 : 택시 미터기 이미지에서 금액 추출
- 기능 2 : 은행 영수증 이미지에서 금액과 정보 추출
- 기능 3 : 혼잡도 분석
기능 1·2 — 이미지에서 정보 추출
모델 선택시 주의할점
- 기능 1,2 :
- 이미지 1개당 60초를 넘어가면 CPU환경 대신 GPU환경을 고려할것.
- 성능기준 탑5개의 후보를 선택하고, 정확도가 크게 다르지 않다면 시간이 압도적으로 적은것을 선택할것
- 단독모델이 더 좋은경우와 파이프라인이 더 좋은경우 측정하고, 성능 저하가 심할경우 원인이 slm에 있다면, 다른 로직을 고려해볼것.
벤치마크 한 모델 리스트
- 테스트한 후보 모델들 리스트
- 800개의 밴치마크중 후보 모델을 추가 측정한 문서를 아래에 제출하였음
| 구분 | 모델 |
|---|---|
| OCR | OvisOCR2 Q4, OvisOCR2 Q8, OvisOCR2 native, PP-OCRv6 medium, PP-OCRv6 small, Surya OCR 2 |
| SLM | LFM2.5-VL-450M native, LFM2.5-VL-450M Q8_0, LFM2.5-VL-3B native, LFM2.5-VL-3B Q4_K_M, LFM2.5-VL-1.6B native, Qwen3.5-0.8B |
실측 문서
- 파라미터만 다르게 실측하였음
최종 평가
- PP-OCRv6 medium -> LFM2.5-VL-1.6B native 조합 선택
- 이유 :
-
- PP-OCR을 제외한 나머지는 정확도는 비슷했으나 타임아웃 문제가 제일 심함.
-
- LFM을 제외한 나머지는 Qwen3.5-0.8B 모델 베이스이나, 마찬가지로 CPU기준 속도가 너무 느림.
-
- 실측 문서에서 PP 계열, LFM 계열의 단독과 조합시 밴치마킹을 측정하였는데, PP-OCRv6 medium -> LFM2.5-VL-1.6B native 조합이 좋은 성능과 빠른 속도를 보임.
-
- SLM의 성능 저하가 제일 크지 않아서 선택하게 되었음.
-
추후 최적화 적용 계획
- 기능 1,2 :
- 현재 포지셔닝 정보가 없는 SLM 모델이라 LFM 모델에 포지셔닝 정보를 추가하는 방향으로 성능개선을 해볼 생각이다.
- json형식으로 항상 답하도록, json 형식을 고정 출력하는 LoRA SFT를 진행해볼 생각이다.
- 양자화를 진행해서 정확도를 유지하고, 속도 향상이 가능한지 여부를 판단할 예정이다.
- CPU 환경이라 정확도에서 약간의 손실이 있더라도, 최대한 속도를 향상시키는 방향으로 고민해보겠다.
- 로직적으로 OCR결과와 slm의 판단을 통해 불일치나 존재하지 않다고 판단할 경우 재추출하는 로직과 기준을 고민해보겠다.
- 단일 OCR 모델만 사용하고, 그 안에서 실제 값을 추출하는 방식도 테스트 해볼 예정이다.
- 실제 예상금액을 계산할수있다면, 추후 그것을 기반으로 범위 안에 들어오는지 판단하는 로직을 구현하는것도 고민중이다.
- CPU를 사용하기 때문에 스케줄링 기법이 중요할것 같다.
- 스케줄링을 단일 작업큐만 사용하는게 아니라 추가적인 방식이 있는지 찾아보고 적용하여 실제 부하 측정도 해보겠다.
기능 3 — 혼잡도 분석
모델 선택 시 확인할 사항
- 1단계의
/congestion-analyses계약을 유지하면서 모델을 교체할 수 있어야 한다. 모델은 신규 신호의 의미를 판정하고 스팟 결과는 코드가 계산한다. - 모든 성능 수치는 기준 부하와 실행 조건을 함께 적는다. 조건이 다른 측정치는 나란히 비교하지 않는다.
- JSON 형식뿐 아니라 신규 신호 ID 누락·중복, 허용된 값과 판정 정확도를 확인한다.
- 60초 주기의 신규 신호를 AI 응답 제한 50초 안에 처리하고 대기가 누적되지 않는지 확인한다.
1. 모델 후보와 선정 방식
운영 API는 외부 제공자의 LLM API를, 자체 서빙은 모델을 직접 GPU에 올려 실행하는 방식을 말한다. 모델이 맡는 일은 게시글·댓글의 관련성, 혼잡 단계, 원인을 판정하는 것이다. 한국어 댓글을 이해하면서 정해진 JSON 형식으로 60초마다 들어오는 요청을 처리해야 한다.
후보는 두 종류다. 실제로 우리 조건에서 돌려본 모델과, 공개 스펙·벤치마크만 보고 고른 모델. 후보에 올랐다고 성능이 검증된 건 아니다. 실제 채택은 같은 부하·평가셋으로 측정한 뒤 성능 평가 계획 기준으로 정한다.
운영 API
현재 gpt-5.6-luna(reasoning low, Responses API + Structured Outputs)를 쓴다. 팀 계정으로 쓸 수 있고 구조화 출력을 지원하며 API 비용도 합리적이다. 실측도 있다 — 동시 호출 5개로 24회를 처리하는 데 12.18초, 형식 통과 24/24였다.
| 구분 | 후보 | 근거 |
|---|---|---|
| 선택 | gpt-5.6-luna |
실측 있음. 위 조건에서 시간 목표를 지켰다 |
| 비교 후보 | Claude Haiku 4.5 | 응답 속도가 빠르고 구조화 출력을 지원한다는 공개 스펙만 확인. 실측 없음 |
| 비교 후보 | Gemini 3.7 Flash | 구조화 출력을 지원하는 다른 제공자 모델이라는 스펙만 확인. 실측 없음 |
비교 후보 두 개는 같은 부하·평가셋·출력 스키마로 실제로 측정한 뒤에만 폴백 모델로 검토한다.
자체 서빙
외부 API 의존도와 비용을 낮출 수 있는지 보는 방안이다. GPU 운영 비용이 따로 들기 때문에 실제 부하에서 비용까지 비교해야 한다. 후보는 아래 기준으로 고른다.
| 기준 | 내용 |
|---|---|
| 라이선스 | 상업적 사용을 막지 않을 것. 비상업 라이선스는 후보 단계에서 표시 |
| 실행 가능성 | 16GB급 GPU 한 장에서 메모리를 감당할 것. 양자화 적용 후 속도·품질도 확인 |
| 문법 제약 디코딩 | 추론 서버가 JSON 구조와 허용 값을 강제할 수 있을 것 |
| 한국어 구어체 | 벤치마크 점수가 아니라 실제 댓글 표본으로 확인 |
| 평가 재사용 | 현재 평가 코드·평가셋을 그대로 쓸 수 있을 것 |
| 후보 | 근거 |
|---|---|
| Qwen3-4B-Instruct-2507 (GPTQ Int4, vLLM) | 실측 있음. T4 16GB에서 24개 동시 처리 16.79초, 정답률 61.1%(FP16 대비 우세) |
| Qwen3.5-4B | Qwen3-4B와 비슷한 크기의 다른 세대 모델이라는 스펙만 확인. 실측 없음 |
| Kanana-1.5-8B-Instruct-2505 | 한국어·영어를 함께 학습했다는 스펙으로 후보에 올림. 초기 실행에서는 출력 형식·처리 시간 모두 기준에 못 미쳐 설정을 바꿔 다시 봐야 한다 |
Qwen3-8B는 초기 실험에서 품질이 좋았지만 최적화한 실행 조건에서는 아직 안 봤다. 그렇다고 후보에서 빼지는 않는다.
2. 분석 구조와 기준 부하
분석 단위와 역할
이미 판정한 신호는 재사용하고 신규 신호만 새로 판정한다. 게시글 묶음은 게시글 1개와 그 게시글에 달린 댓글이며 post_id로 연결한다. 스팟은 같은 위치의 게시글 묶음들을 모아 최종 혼잡도를 표시하는 단위다.
댓글 100개가 처리된 게시글 묶음에 새 댓글 3개가 추가되면, Spring은 원글과 현재 유효한 댓글 전체(기존 100개 + 새 댓글 3개)를 매 요청에 다시 담아 보낸다. 모델은 새 댓글 3개만 판정하고, 코드가 보관된 판정과 합쳐 스팟 결과를 계산한다. 원글이 항상 함께 오기 때문에 부모 문맥이 없어 해석하기 어려운 댓글은 생기지 않는다.
| 구분 | 역할 |
|---|---|
| 모델 입력 | 위치 정보, 요청으로 받은 게시글 묶음 원문(원글+현재 댓글), 신규 신호 식별 정보 |
| 모델 출력 | 신규 신호별 관련성·혼잡 단계·원인 문구(40자 이내). kind와 signal_id에 연결 |
| 코드 처리 | 최근 20분의 유효 제보에서 작성자별 최신 제보를 골라 최다 단계를 최종 등급으로 계산. 동률이거나 유효 제보가 없으면 UNKNOWN |
| Spring에 보낼 요약 | 대표 신호의 원인 문구를 그대로 cause_summary로 사용. 문구는 모델이 생성하고 코드는 대표 신호만 고른다 |
AI는 원문을 따로 보관하지 않는다. Spring이 매 요청마다 게시글 묶음 원문 전체를 다시 보내기 때문에, 누적 댓글이 늘어날수록 입력 토큰 수도 함께 늘어난다. 이 토큰 수 증가와 처리 시간은 측정 대상이다.
1단계 계약과의 연결
Spring은 60초마다 /congestion-analyses로 신규 신호를 보내고 같은 요청의 응답으로 스팟 결과를 받는다. AI 응답 제한은 50초, Spring 대기는 55초다.
- 모델 출력은 AI 내부의 신호별 판정이다. 응답의 스팟 결과와 구분한다.
- 코드는 최종 결과에 들어갈
congestion_level,cause_code,cause_summary,report_count,last_reported_at을 계산한다. - 의미를 확정할 수 없어
UNKNOWN으로 판정한 경우(정상 처리 결과)와, 모델 호출·출력 검증에 실패해 판정을 못 만든 경우(analysis_status: FAILED)는 구분한다. FAILED 스팟은 새 결과를 내지 않고 Spring이 마지막 정상 결과를 유지한다. - 신규 신호 없이 시간 경과만으로 재집계하는 주기에는 LLM을 호출하지 않는다.
- AI가 보관 중인 판정 자체를 잃으면
409 STATE_RESET을 반환한다. 이때 Spring이 전체를 재전송하고, 전체 분석에 성공해야 정상 처리로 돌아간다.
기준 부하
모든 성능 수치는 아래 세 부하 중 하나를 명시해 기록한다. 스팟 6개는 팀 합의값이고 나머지는 설계 가정이라, 실제 트래픽 추정이 나오면 갱신한다.
| 부하 | 스팟 | 주기당 입력 신호 | 게시글 묶음당 누적 댓글 | 캐시 | 발생 상황 |
|---|---|---|---|---|---|
| 정상 | 6 | 6 (스팟당 1) | 10 내외 | 적중 | 운영 시간 대부분 |
| 피크 | 6 | 30 (한 스팟 집중) | 20 이상 | 적중 | 이벤트 종료 등 |
| 콜드 | 6 | 120 (재전송분 포함) | — | 비어 있음 | 재배포 후 분석 상태를 복구하는 주기 |
콜드 부하의 120건은 신규 작성량이 아니라, 분석 대상 시간 범위에서 재전송받아 다시 분석할 게시글·댓글 수다.
본문 길이는 세 부하 공통으로 짧은 글 위주에 500자를 한두 건 섞는다. 모든 입력 문장은 서로 달라야 한다. 같은 문장을 반복하면 모델 서버가 동일한 입력 앞부분의 계산 결과를 재사용해서, 처리 시간이 실제보다 짧게 나올 수 있다. 이는 추론 서버의 입력 계산 재사용 기능이며, AI가 신호별 판정만 보관하는 분석 캐시(3단계)와는 다른 개념이다.
실행 조건으로 추론 서버와 버전, 양자화 방식, 동시 호출 수, 출력 길이 상한, 입력 계산 재사용 on/off를 고정한다. 측정치를 적을 때 이 다섯 가지를 함께 적는다.
현재 합성 평가셋 60건은 본문이 30~50자로 짧다. 짧은 댓글부터 계약 상한인 500자 본문까지 포함하도록 보완한 뒤, 사람이 관련성·혼잡 단계의 정답을 검토해 고정한다. 그 전까지의 품질 수치는 오답 유형을 파악하는 참고 자료로만 쓴다.
3. 예상 병목과 근거
아래 병목은 이전 설계·실행 조건에서 나온 신호를 근거로 예상한 것이다. 확정된 현재 성능이 아니라, 재측정에서 먼저 확인할 지점이다.
| # | 병목 | 예상 근거 | 원인 |
|---|---|---|---|
| 1 | 출력 형식 | 자체 서빙 후보에서 형식 통과율이 84.4~93.8%로 100%에 못 미쳤다(운영 API는 같은 측정에서 24/24 통과) | 원문 ID 누락과 허용되지 않은 값 출력. 프롬프트 지시만으로는 강제되지 않는다 |
| 2 | 판정 품질 | 관련성·혼잡 단계 정답률이 37~61%로 측정마다 편차가 컸다 | 오답 다수가 관련 여부 혼동. 정답 기준(위치 범위·관련성)이 팀과 맞춰지지 않았다 |
| 3 | 양자화·실행 설정 | 같은 GPU에서 초기 969.46초였다가 설정을 바꾼 뒤 48.83초, 24개 동시 처리 평균 16.79초까지 줄었다 | 양자화·GPU 계산·동시 처리·입력 재사용 설정을 한 번에 바꿔서 원인별 효과는 분리 못 했다 |
| 4 | 측정 신뢰도 | 16.19초는 입력 24개가 동일 문장이라 재사용률 95.2%가 섞인 값이다 | 서로 다른 입력으로 다시 재야 실제 처리 시간을 알 수 있다 |
| 5 | 긴 게시글 묶음 | 댓글 60개 게시글 묶음 하나가 114.6초, 댓글 20개도 20.8초로 응답 제한 50초의 약 42%를 썼다 | 누적 댓글이 많은 게시글 묶음은 입력 처리에 시간을 많이 쓴다 |
| 6 | 콜드 주기 | 재배포 후 분석 상태를 복구하면서 재분석할 원문이 늘어난다 | 분석 상태가 프로세스 수명에 묶여 있다 |
3번은 실행 설정을 바꿔서 처리 시간을 줄인 사례지만 지금 부하에서는 아직 검증하지 않았다. 출력 형식 문제는 자체 서빙 후보에서 이미 관찰됐으니 우선 확인하고, 속도와 판정 품질도 재측정해야 자체 서빙 전환을 판단할 수 있다.
4. 최적화 계획
| 순서 | 기법 | 대상 병목 | 기대 효과 | 검증 방법 |
|---|---|---|---|---|
| 1 | 문법 제약 디코딩 (vLLM guided decoding)과 출력 검증 | 1 | JSON 구조와 허용 값을 제한해 형식 오류 감소. 입력 ID와 출력 ID의 일치 여부는 코드로 따로 검증 | 같은 평가셋으로 on/off 비교. ID 누락·중복과 출력 토큰·시간 변화도 함께 기록 |
| 2 | 정답 기준 합의 후 평가셋 재작성 | 2 | 팀이 합의한 정답으로 모델별 판정 품질 비교 | 최대 500자까지 다양한 길이로 보완, 사람 검토, 재측정 |
| 3 | 동시 호출 수 조정 | 4, 5 | 부하별 최적값 확인, 제공자의 요청·토큰 제한도 함께 고려 | 정상·피크·콜드 각각에서 2·5·12·24 비교 |
| 4 | 게시글 묶음 입력 상한과 분할 | 5 | 긴 게시글 묶음의 처리 시간을 주기 안으로 제한 | 누적 댓글 8·20·60에서 시간·품질 측정 후 상한 결정 |
| 5 | 콜드 주기 완화 | 6 | 재배포 후 재분석 작업이 한 주기에 몰리는 부담 감소 | 재전송분을 여러 주기에 나누는 방식과 부분 결과 응답 비교 |
| 6 | 규칙 기준선 비교 | 2 | LLM을 쓸 자리와 안 써도 되는 자리를 숫자로 가른다 | 키워드·부정어 규칙과 경량 분류기를 같은 평가셋으로 측정 |
| 7 | 배치 프롬프팅 도입 검토 | - | 여러 게시글 묶음을 한 호출로 묶어 호출 수·오버헤드를 줄인다. 서로 다른 게시글 묶음의 문맥이 섞여 판정이 흐트러질 위험은 게시글 묶음 구분자·ID 재확인으로 완화한다 | 배치 크기(한 호출에 묶는 게시글 묶음 수 1·4·8·16)별로 정확도와 지연시간을 같은 평가셋으로 비교 후 결정 |
| 8 | 프롬프트 캐싱 활용 | - | 시스템 프롬프트 등 매 호출 반복되는 고정 부분을 캐싱해 지연시간·비용 절감 | 캐싱 on/off를 같은 부하에서 비교 |
| 9 | 출력 스키마 경량화 | - | 출력 필드명 축약, 불필요한 설명·필드 제거로 응답 토큰 수 자체를 줄임 | 스키마 변경 전후 출력 토큰 수·처리 시간·형식 통과율 비교 |
| 10 | 무의미 신호 사전 필터링 | - | 공백·특수문자만 있는 신호는 LLM 호출 없이 즉시 IRRELEVANT 처리해 호출 수를 줄임 | 평가셋에서 제외 대상 비율과 오제외 비율 확인 |
6번은 다른 최적화와 별도로 진행할 비교 실험이다. 키워드·부정어 규칙이나 경량 분류기가 같은 평가셋에서 어느 정도 맞히는지 재고 LLM을 썼을 때 얼마나 나아지는지 확인한다. 원글 문맥이 필요한 짧은 댓글은 따로 평가해서, 사전 분류기가 이런 댓글을 잘못 제외하지 않는지 봐야 한다. 7~10번은 병목 표에 없는 새 관점(호출 수·캐싱·출력 경량화·사전 필터링)을 다루며, 이들도 하나씩 적용하고 같은 기준 부하로 측정한다.
vLLM의 구조화 출력은 지정한 스키마에 맞춰 생성을 제한한다. 다만 JSON 스키마를 적용하는 것만으로 모든 입력 ID가 한 번씩 반환되거나 의미 판정이 정확해지는 건 아니다. ID 대조 검사와 정답 평가는 따로 유지한다. 설정 이름과 지원 범위는 쓸 vLLM 버전에서 확인한다.
1, 2번이 품질을, 3~5번이 시간을 다룬다. 기법을 한 번에 여러 개 바꾸면 기여도를 분리할 수 없으므로 순서대로 적용하고 매 단계 같은 기준 부하로 측정한다.
운영 API 쪽에서 함께 볼 것
- 문맥 입력량: 요청에 포함되는 게시글 묶음 원문의 범위와 신규 신호량을 바꾸어 정확도·토큰·지연을 함께 측정한다.
- 실패 비용: 총 호출 수·성공률·지연을 측정할 때 다음 주기 재시도도 포함한다.
- 선택적 사전 분류: 게시글 묶음 본문을 LLM 분석 전에 가벼운 분류기로 1차 분류해 컨텍스트를 줄이는 방안이다(규칙 기준선 비교와 연결). TF-IDF + Linear SVM과 Jev API(6단계) 후보의 정확도·비용을 같은 평가셋으로 비교한 뒤 더 나은 쪽을 도입한다. 짧은 댓글(예: "저도요")을 잘못 제외하는 비율부터 확인하고, 확인 전에는 필수 단계로 넣지 않는다.
자체 서빙 전환 준비
- 현실적인 입력: 서로 다른 신규 내용과 요청으로 받은 게시글 묶음 원문으로 입력 계산 재사용 유무와 메모리 사용을 비교한다.
- 출력·판정 품질: 출력 형식 제약과 ID 검증, 관련성·원인·혼잡도 오판을 각각 평가한다.
- 실행 환경: GPTQ 계산 구현 경고를 확인하고 모델 파일·버전·GPU 실행 설정을 고정한다.
- 지속 부하: 여러 게시글 묶음의 동시 유입과 한 게시글 묶음의 대량 신규 유입을 최소 100주기 동안 측정한다.
- 전환 판단: 검토를 마친 동일한 평가셋과 기준 부하로 운영 API와 비교한 뒤 결정한다.
5. 성능 평가 계획
| 지표 | 현재 | 목표 | 판정 조건 |
|---|---|---|---|
| 정상 주기 처리 시간 | 미측정 | AI 응답 50초 이내, 여유 확보 | 100주기 연속에서 P95가 목표 이내이고 대기가 누적되지 않음 |
| 피크 주기 처리 시간 | 미측정 | 50초 이내 응답. 미완료 작업이 있으면 부분 결과로 처리 | Spring이 마지막 정상 결과를 만료 기준까지 유지하고 실패한 후보 등급으로 덮어쓰지 않음 |
| 콜드 주기 처리 시간 | 미측정 | 응답 제한 50초는 유지하고 분석 상태 복구는 다음 주기까지 이어질 수 있음 | 복구 완료까지 걸린 주기 수와 재전송량 기록 |
| 형식 통과율 | 미측정 | 100% | 평가셋 전량 통과 |
| 관련성·혼잡 단계 정답률 | 미측정 | 사람이 검토한 평가셋 기준으로 운영 API 이상 | 사람 검토된 평가셋 기준 |
| HIGH를 LOW로 오판 | 미측정 | 0에 근접 | 별도 집계, 허용 기준은 팀 합의 |
P95는 측정값의 95%가 그 값 이하라는 뜻이다. 이 표는 앞으로 검증할 목표를 적어둔 것이지, 지금 성능을 보장하는 게 아니다.
자체 서빙 전환 조건
세 조건을 모두 만족할 때만 전환을 논의한다.
- 기준 부하 정상·피크에서 시간 목표 충족.
- 형식 통과 100%.
- 같은 평가셋에서 판정 품질이 운영 API 이상.
세 조건 모두 구현 후 같은 부하·평가셋으로 다시 측정해서 판단한다.
6. 최종 평가
- 운영은
gpt-5.6-luna를 유지한다. 비교 후보(Claude Haiku 4.5, Gemini 3.7 Flash)는 같은 조건으로 실측한 뒤 폴백 모델로 쓸지 결정한다. - 자체 서빙 후보(Qwen3-4B, Qwen3.5-4B, Kanana)도 제시만 해둔 상태다. 전환 조건 세 가지를 충족하면 외부 API 계약과 집계 규칙은 유지하면서 모델 호출 계층만 교체한다.
- 이전에 측정한 수치는 지금과 다른 설계·실행 조건에서 나온 값이라 현재 성능으로 쓰지 않는다. 기준 부하(정상·피크·콜드)로 다시 잰다.
- 예상 병목은 출력 형식, 판정 품질, 긴 게시글 묶음, 콜드 주기다. 출력 형식 문제는 자체 서빙 후보에서 이미 관찰됐고, 나머지는 지금 부하에서 검증해야 한다.
- 최적화는 문법 제약 디코딩부터 적용하고, 단계마다 같은 기준 부하로 측정한다.