병렬 처리로 On-Device LLM 최적화하려다가...

k_bell·2026년 1월 21일

DocWeave

목록 보기
3/5
post-thumbnail

RAG PDF 분석 서비스 'DocWeave' 개발 과정에서의 트러블슈팅

지난 글에서는 LLM의 환각 현상을 잡기 위해 Validation 로직을 도입한 과정을 소개했습니다. 검증을 통해 응답의 신뢰성은 확보했지만, 새로운 문제가 발생했습니다. 바로 '속도(Latency)'입니다.

로컬 환경(Galaxy Book 3 Pro, i5-1340p, iris Xe)에서 LLM이 답변을 생성하는 데만 10초 이상이 걸리는데, 여기에 ContextAnswer를 임베딩하고 비교하는 검증 시간(약 3~4초)까지 더해지니 사용자가 체감하는 대기 시간이 너무 길어졌습니다.

이번 글에서는 속도 문제를 해결하기 위해 병렬 처리(Parallel Processing)를 적용했지만, 오히려 성능이 저하된 사례와 그 원인을 정리하고자 합니다.

1. "LLM이 생각하는 동안, 미리 계산하자"

기존 로직은 정직한 순차 처리(Sequential Processing) 방식이었습니다.

  1. LLM이 답변 생성
  2. 문서(Context) 임베딩 변환
  3. 답변(Answer) 임베딩 변환 및 유사도 비교

여기서 2번 과정인 '문서 임베딩'은 LLM의 답변과 무관하게 미리 수행할 수 있는 작업입니다. 따라서 "LLM이 답변을 생성하는 동안(I/O), 백그라운드에서 문서 임베딩(CPU/GPU)을 동시에 수행하면 약간의 시간을 벌 수 있지 않을까?" 라는 가설을 세웠습니다.

2. 구현: CompletableFuture를 이용한 병렬화

Java의 CompletableFuture를 사용하여 두 작업을 비동기로 동시에 실행하도록 코드를 수정했습니다. A/B 테스트를 위해 Feature Flag를 두어 실시간으로 모드를 변경할 수 있게 구현했습니다.

// Feature Flag에 따른 분기 처리
if (useOptimization) {
    log.info("🚀 [Mode: Optimized] Executing Parallel Processing...");
    
    // Task 1: LLM 답변 생성
    CompletableFuture<String> answerFuture = CompletableFuture.supplyAsync(() ->
            chatClient.prompt(prompt).call().content()
    );

    // Task 2: (동시에) Context 미리 임베딩 
    CompletableFuture<float[]> contextEmbeddingFuture = CompletableFuture.supplyAsync(() ->
            embeddingModel.embed(finalContext)
    );

    // 두 작업이 끝날 때까지 대기 (join)
    CompletableFuture.allOf(answerFuture, contextEmbeddingFuture).join();
    
    // ... 이후 검증 로직 수행
}

이론상으로는 Context Embedding 시간이 LLM Generation 시간 뒤로 숨겨지면서(Hiding Latency), 전체 응답 속도가 개선되어야 했습니다.

3. 결과: 예상 밖의 성능 저하

하지만 StopWatch를 찍어본 결과는 다음과 같았습니다.

🐢 [Legacy] 순차 처리 결과

  • 총 소요 시간: 약 105초

🚀 [Optimized?] 병렬 처리 결과

  • 총 소요 시간: 약 112초

오히려 병렬 처리를 적용한 쪽이 약 7초 더 느려졌습니다.

4. 원인 분석: 자원 경합 (Resource Contention)

원인은 바로 실행 환경(On-Device)의 하드웨어적 특성에 있었습니다.

서버 환경이나 고사양 GPU가 장착된 데스크톱에서는 병렬 처리로 성능 향상을 기대할 수 있었겠지만, 제 개발 환경은 내장 그래픽(iGPU)을 사용하는 노트북이어서 그 효과를 얻기 어려웠습니다.

1) 1차선 도로의 병목 현상

로컬 LLM(llama3.2)을 구동하는 것은 이미 시스템의 메모리 대역폭(Memory Bandwidth)연산 장치(Compute Units)를 100% 한계까지 사용하는 작업입니다.

  • 순차 처리: 거대한 덤프트럭(LLM)이 지나간 뒤, 승용차(Embedding)가 지나감. → 원활
  • 병렬 처리: 1차선 도로에 덤프트럭과 승용차를 동시에 밀어 넣음. → 충돌 및 정체

2) Context Switching 오버헤드

OS와 하드웨어 관점에서 보면, LLM 연산 도중 임베딩 연산이 끼어들면서 빈번한 컨텍스트 스위칭이 발생했습니다. 이미 자원이 포화된 상황에서 이러한 오버헤드는 치명적이었고, 그 결과 LLM 생성 속도가 96초에서 107초로 오히려 느려지는 부작용이 나타났습니다.

5. 결론 및 해결책

이번 경험을 통해 "소프트웨어 아키텍처는 하드웨어 컨텍스트에 의존한다"는 중요한 교훈을 얻을 수 있었습니다.

서버 사이드 개발에서는 I/O가 잦기 때문에 비동기/병렬 처리가 대부분 성능 이득을 주지만, On-Device AI 환경, 특히 자원이 제한적인 로컬 PC에서는 순차 처리가 오히려 더 빠를 수 있음을 확인했습니다.

0개의 댓글