뭐요

스프링에서 OpenAI API 성능 개선기 본문

Spring

스프링에서 OpenAI API 성능 개선기

욕심만 많은 사람 2023. 9. 3. 01:16

라이브러리 사용


  • 정식 지원 ⇒ Python, NodeJS
  • 따라서 AWS Lambda 활용

모델 선택의 고민


추천 사유 요약에 대한 목적에 맞게 모델을 변경하며 적합한 모델을 heuristic하게 찾아감.

1. text-davinci-003

  • 엄청엄청 느림…!

2. gpt-4

  • 한번의 API 호출 ⇒ 10.95s
request_input = ' '.join(event) + " 이 문장들을 정확히 하나의 문장으로 요약해줘. 그리고 항상 '~해요'체로 끝나도록 요약해줘."
response = openai.ChatCompletion.create(
  # 사용 모델
  model="gpt-4",
  messages=[
        {"role": "user", "content": request_input}
    ]
)

3. gpt-3.5-turbo

  • 한번의 API 호출 ⇒ 3.41s
  • “~해요”체로 요약하지 않는 경우가 빈번함
request_input = ' '.join(event) + " 이 문장들을 정확히 하나의 문장으로 요약해줘. 그리고 항상 '~해요'체로 끝나도록 요약해줘."
response = openai.ChatCompletion.create(
  # 사용 모델
  model="gpt-3.5-turbo",
  messages=[
        {"role": "user", "content": request_input}
    ]
)

🧟‍♀️
왜 gpt-3.5 turbo를 사용했는가?
  • 모델을 우리의 목적에 맞게 fine-tuning 해야 함
  • (우리의 목적은 10개의 추천 사유를 1개의 완전한 “~해요”체 문장으로 요약해야 함)
  • gpt-4는 fine-tuning 지원을 아직 안하기 때문에 공식문서에서 권장하는 gpt-3.5 turbo 모델로 결정

프롬프트 엔지니어링


🧟‍♀️
fine-tuning을 통해 프롬프트 엔지니어링을 진행했습니다.
  • 모델에게 역할을 부여함
  • 모델을 훈련시킬 dataset을 사전에 제공함
response = openai.ChatCompletion.create(
  # 사용 모델
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "항상 '해요'체로 끝나도록 요약해줘"},
				# 사전 학습
        {"role": "user", "content": "블랙톤 휠은 차량의 드라이브 편의성을 높여줍니다. 고급 차량의 필수 액세서리로 꼽히는 이유가 있습니다. 이 문장을 고객에게 추천하듯이 하나의 문장으로 요약해줘."},
        {"role": "assistant", "content": "차량의 드라이브 편의성을 높여주는 고급 차량의 필수 액세서리에요."},
        {"role": "user", "content": request_input}
    ]
)

Spring에서 API 호출

  • 사용자에게 두 개의 옵션을 추천해주어야 하기 때문에, 2번의 OpenAI API를 호출해야 했음

1. 동기 호출 by OpenFeign

  • API 2번 호출 시 3.542s + 알파 (쿼리시간) 예상함

⇒ 예상과 일치

⇒ 하지만 아직도 너무 느리다.!!!!!!!!!!

2. 비동기 호출 by OpenFeign

  • ComletableFuture 객체를 사용해서 비동기로 OpenAI API 2번 호출
  • concurrent package에서 WorkStealingPool 쓰레드 풀 생성
  • 아래 LOG에서 서로 다른 쓰레드에서 동작한 것을 확인할 수 있음
👉🏼
Code
👉🏼
Try

ComletableFuture 객체

  • Future 객체의 한계점을 극복
    • 예외 처리 불가
    • 여러 Future 조합 불가능
  • runAsync
    • 반환값이 없는 경우
    • 비동기로 작업 실행 콜
  • supplyAsync
    • 반환값이 있는 경우
    • 비동기로 작업 실행 콜
  • exeptionally
    • 발생한 에러를 받아서 예외를 처리함
    • 함수형 인터페이스 Function을 파라미터로 받음

Executor

  • 개발자가 하나하나 Thread를 관리하기 힘듦
  • 따라서 쓰레드를 만들고 관리하는 작업을 Executor에게 역할 위임
👉🏼
Executor의 역할
  1. 생성 : application에서 사용될 Thread를 만들거나, 쓰레드 풀을 통해 관리합니다.
  1. 관리 : Thread 생명 주기 관리
  1. 작업 처리 및 실행 : Thread로 작업을 수행하기 위한 API 제공

Executors

  • Executor를 생성하는 팩토리 역할
  • Thread Pool을 손쉽게 생성

Thread Pool

  1. newFixedThreadPool()
    1. 고정된 쓰레드의 개수를 갖는 쓰레드 풀을 생성합니다.
    1. ExecutorService 인터페이스를 구현한 ThreadPoolExecutor 객체가 생성됩니다.
  1. newCachedThredPool()
    1. 필요할 때 필요한 만큼의 쓰레드를 풀 생성합니다. (고정X)
    1. 이미 생성된 쓰레드가 있다면 이를 재활용 할 수 있습니다.
    1. 60초 동안 사용되지 않으면 쓰레드를 종료시킵니다.
  1. newWorkstealingpool()
    1. 인자를 통해 병렬 처리 레벨을 지정합니다.
    1. 인자를 지정하지 않으면 현재 시스템의 core 프로세스 개수 기반으로 pool 사이즈가 할당됩니다.

WorkStealingPool

  • 소프티어 부트캠프 WAS 구현 미션에서 백엔드 마스터님이 concurrent package에 대해 간단하게 가르쳐주셨고, 당시에 따로 학습을 했었음! ⇒ https://dmansp.tistory.com/69
  • 현재 시스템의 core 프로레스 개수 기반으로 pool 사이즈를 할당하는 것이 확장성에 용이할 것이라 판단하여 결정