NAVER Place에서는 Place 영역에 특화된 SLM Vertical Service를 운영하여 플레이스 프로덕트 전반(플레이스, 지도, 여행)의 사용성을 개선하고 있습니다.
이 글에서는 NVIDIA와 NAVER가 SLM Vertical Service 운영을 위해 TensorRT-LLM 으로 최적화한 기법과 Triton server를 활용해 실제 서비스를 운영했던 경험과 노하우를 다룹니다. 아래는 서비스에 대한 간단한 소개입니다. 추가적인 디테일은 Introduction to NAVER Place AI Development Team을 참조바랍니다.
NAVER Place 리뷰의 SLM
SLM이란 거대 언어 모델(LLM)에 비해 상대적으로 적은 수의 매개변수로 구성되어 자연어 콘텐츠를 처리, 이해, 생성할 수 있는 언어모델을 의미합니다. SLM은 보통 Fine-tuning을 통해 더 적은 컴퓨팅 자원과 메모리를 소비하면서 특정 작업과 영역에 대해 높은 성능을 발휘할 수 있는 것으로 알려져 있습니다.
NAVER Place에서는 사내에 보유하고 있는 양질의 in-house dataset을 통해 SLM을 학습하여 NAVER Place 사용자들이 남겨놓은 방문자 리뷰를 요약해 주거나 해당 장소에 대한 마이크로 리뷰를 제공하고 있습니다.

SLM transformer decoder 기반 POI 매칭
Naver Place에서는 플레이스 등록 업체의 분산된 영수증, 카드 내역 등을 수집하여, 고객의 방문 내역과 리뷰를 지도에 노출해주는 서비스를 제공합니다. 이를 위해 고객의 방문 내역을 플레이스 등록 업체 POI(Place Of Interest)와 매칭하는 시스템을 제공합니다. 또한 이 시스템은 블로그 글에서 신규 POI를 발견하거나, POI 간 중복을 식별하여 데이터의 무결성을 유지하고 품질을 향상시키는 데 활용됩니다. 이 시스템은 Retrieval 구조의 검색 시스템에 SLM transformer decoder를 도입하여 최신 POI에 대한 검색 정보와 생성 모델을 결합하여 더욱 신뢰할 수 있는 서비스를 제공합니다.

Inference 성능 최적화를 위한 NVIDIA TensorRT-LLM 도입
TensorRT-LLM은 NVIDIA GPU에서의 고성능 추론을 위해, 모델 그래프를 최적화하고 runtime engine을 빌드하는 llm inference 최적화 솔루션입니다. In-flight batching을 지원하여 처리량을 극대화하고, auto-regressive 모델의 특성에 맞게 paged KV cache, chunked context와 같은 메모리 최적화 기법을 적용하여 메모리 사용 효율을 높입니다.
다른 LLM inference 솔루션들에 비해 Throughput, TTFT(Time to First Token), TPOT(Time Per Output Token) 등에서 모두 우세한 성능을 보여 해당 솔루션을 기술 스택으로 가져가게 되었습니다. 아래는 qwen model로 측정한 다양한 input, ouput token len에 대한 A100과 H100에서의 TensorRT-LLM과 Alternative open-source LLM inference library 간의 Throughput 비교입니다.

decode-prefill light, prefill heavy, decode heavy, decode-prefill heavy 모든 조건의 작업 유형에서 TensorRT-LLM이 우세한 것을 알 수 있습니다. 이중에서 SLM에 대한 decode heavy task가 가장 좋은 성능을 보였고, TensorRT-LLM에서 최신 GPU에 대한 최적화된 커널을 제공하고 있는 만큼 NVIDIA Hopper architecture에서 성능이 특히 좋았습니다. 성능 측정방식에 대해 좀 더 자세한 내용은 NVIDIA/TensorRT-LLM Github의 perfornace overview, TensorRT-LLM engine을 빌드하는 것과 관련해서는 Best practices for tuning 를 참조하시기 바랍니다.
Inference 최적화: throughput, latency 간의 trade-off
이 섹션에서는 batch size paged KV cache 및 in-flight batching 등의 memory 최적화 기술 측면에서 LLM 추론에서의 처리량과 지연 시간을 균형 있게 맞추는 전략을 다룹니다.
Batch의 크기
LLM 추론 서버는 기본적으로 batch로 요청을 처리하여 처리량을 극대화하지만 반대로 latency가 증가하는 trade-off가 있습니다. 때문에 서비스 요구사항에 따라 목표 TTFT 및 TPOT를 설정하고 batch size를 조절하며 운영해야 합니다.

Paged KV cache 및 in-flight batching
TensorRT-LLM은 기본적으로 paged kv cache 옵션이 활성화 돼있어 memory 사용을 효율화 하여 최대로 허용되는 batch의 크기를 높이는 효과가 있어 높은 처리량을 요구하는 task는 물론 낮은 latency를 요구하는 task에서도 일반적으로 좋은 효과를 보입니다.
In-flight Batching 또한 기본적으로 활성화 되어있어 throughput을 개선하는 효과가 있기 때문에 대부분의 task에서 두 옵션을 default로 사용하고 있습니다.
다만, 한 가지 예외 사례로서 모델의 크기가 비교적 작고 상대적으로 구형의 GPU를 사용하여 극단적으로 낮은 latency로 서비스를 운영해야 하는 경우에는 위 두 옵션을 off 하는 것이 이득인 경우가 있었습니다. 예를 들면, POI 매칭의 경우 실시간으로 요청을 처리해야 하여 극단적으로 낮은 latency가 요구되었습니다. 모델의 파라미터 수가 1.3B로 비교적 작았으나 GPU 수급 이슈로 인해 비교적 구형 아키텍처인 T4를 사용해야 했기에 batch의 크기를 1로 주어야만 요구 latency를 맞출 수 있었고, batching option은 off해야 했습니다.
또한 1.3B 정도의 작은 model을 batch size 1로 운영할 경우 paging overhead가 compute overhead보다 커서 latency와 qps에 손실이 있었습니다. 이 경우 모델 크기가 작고 batch의 크기 또한 1이었기 때문에 memory overhead 측면에서 비교적 유연했기에 paged kv cache를 비활성화하고 contiguous kv cache로 운영이 가능했습니다.
precision | paged kv cache | cache blocks | input/output | max batch_size | QPS | latency(secs) |
fp16 | on | 7110 | 500/5 | 1 | 6.49 | 0.154 |
fp16 | off | 7110 | 500/5 | 1 | 8.39 | 0.119 |
POI 매칭의 경우 latency가 중요한 실시간 서비스 외 에도 throughput이 중요한 background 매칭 사용성이 요구되는 경우가 있어 용도에 맞는 옵션으로 빌드하여 각각 운영하고 있습니다.
"build_config": {
"max_input_len": 512,
"max_output_len": 32,
"max_batch_size": 1,
"max_beam_width": 1,
"max_num_tokens": 4096,
...
"plugin_config": {
...
"paged_kv_cache": false,
...
}
}
"build_config": {
"max_input_len": 512,
"max_output_len": 32,
"max_batch_size": 8,
"max_beam_width": 1,
"max_num_tokens": 4096,
...
"plugin_config": {
...
"paged_kv_cache": true,
...
}
}
Inference 최적화: Downstream caching
이 섹션에서는 캐싱 기법을 활용하여 다운스트림 추론 작업을 간소화하는 최적화 전략에 대해 다룹니다. prefix caching 및 response caching으로 중복 연산을 줄이고 전반적인 효율성을 향상시킬 수 있습니다.
Prefix caching
Downstream task에서 입력으로 들어오는 prompt에는 공통 prefix가 있기 때문에 요청마다 전체 prefill을 계산하는 것은 낭비일 수 있습니다. 이를 방지하기 위해 trt-llm은 prefix caching 기능을 제공합니다. 기능을 사용하면 memory와 연산량을 상당부분 줄일 수 있습니다. 자세한 내용은 TensorRT-LLM Github에서 제공하는 how to enable KV cache reuse를 참조하시기 바랍니다.
이는 TTFT를 크게 개선할 수 있기 때문에 Input의 길이가 길고 system prompt가 공유되며 출력의 길이가 짧은 task들에 적용하면 큰 효과를 볼 수 있습니다. 특히 마이크로 리뷰의 경우 하나의 마이크로 리뷰를 생성하기까지 평균 40회의 mulit-step inference가 필요하고, 각 step들은 prefix를 공유하기 때문에 큰 효과를 볼 수 있었습니다.
반면에 system prompt가 크게 다른 여러 task의 요청을 받아야 한다면 cache가 LRU로 관리되기 때문에 cache 효과는 적고 관리 오버헤드가 생길 수 있으니 주의해야 합니다.
Response caching
Triton server의 response caching 기능으로 비효율적인 중복 추론을 방지할 수 있습니다.
모델 이름, 버전, 입력을 기반으로 추론 요청의 해시를 생성하고, 이를 통해 응답 캐시에 접근하는 방식입니다. multi nomial sampling decoding과 같이 의도적으로 재추론이 필요한 경우가 아니면 response caching을 적용하여 효율적으로 운영할 수 있습니다. 실시간으로 운영하고 있는 POI 매칭의 경우 초당 4~5회의 cache hit이 발생하여 요청에 대한 연산이 17% 절약되고 있습니다. 자세한 내용은 Triton Response Cache documentation을 참고하시기 바랍니다.

Triton을 통한 TensorRT-LLM serving
TensorRT-LLM을 통해 빌드한 SLM 엔진은 NVIDIA의 NVIDIA Triton Inference Server를 사용하여 서비스하고 있습니다. 특히 tokenizing, postprocessing, multi-step 추론 등의 파이프라인을 구성하기 위해 Ensemble model 또는 BLS를 사용할 수 있습니다. 그 중 더 유연한 수정이 가능한 BLS를 선택하여 운영중입니다. 이번 절에서 소개할 내용은 Triton BLS의 장점을 극대화하고 사용편의성을 증대시킨 경험입니다.
잘 정의된 request/response schema를 통한 사용성 향상
Triton 모델은 기본적으로 pb_tensor
형식으로 데이터를 주고받습니다. 통신 효율과 LLM 추론 최적화를 위해 채택된 BLS 구조에는 전처리와 후처리 코드가 포함되어 있으며, 이를 처리하기 위해 pb_tensor
를 NumPy 배열로 변환하고 다시 pb_tensor
로 재변환하는 작업이 필요합니다.
이 과정에서 두 가지 애로사항이 있습니다. 첫째, 각 모델의 입출력 데이터의 유효성 검증이 없다면 데이터 형식 오류나 필수 필드 누락이 런타임에서야 발견되어 디버깅이 어렵습니다. 둘째, BLS에 모든 전후처리를 통합하면서 코드 복잡도가 증가해, 모델 간 호출 의존성이 있는 경우 확장성과 유지보수가 크게 어려워집니다. 가령 POI 매칭의 경우 아래 그림과 같이 복잡한 파이프라인을 거치게 됩니다.

BLS 내에서 POI 매칭 추론 파이프라인은 파싱된 OCR을 시작으로 임베딩 추출(tokenization, BERT encoder), 벡터 검색(전후처리 및 retrieval), re-ranking (tokenizer , Reranker model), 그리고 마지막으로 응답 생성(generator encoder and decoder)으로 이어지는 multi-step process를 거칩니다. 해당 sequence는 pb_tensor와 numpy format 간의 데이터 변환과 다양한 전후처리 작업에 수반되는 복잡성과 상호 의존성을 보여줍니다.이 문제를 해결하기 위해 아래와 같은 접근을 채택하였습니다
IO schema 관리 표준화
NVIDIA에서 제공하는 Python Dataclass 사례를 참고하여, 유효성 검사가 용이한 Pydantic을 활용해 입력 및 출력 스키마를 정의하였습니다. 이를 통해 모든 Triton 모델의 요청과 응답에 대해 구조적 일관성을 보장하고 데이터 검증을 강화했습니다.
예를 들어, 아래와 같은 BlsRequest
클래스를 정의하여 Triton 요청의 입력 데이터 형식을 관리하고 유효성 검사를 수행하도록 설계하였습니다.
# NOTE: Because Triton uses pb_tensor and Numpy objects,
# it is required to declaratively manage the fields that are not defined as Python default types.
# For this, we added tTe json_schema_extra field of Pydantic to explicitly manage data types.
class BlsRequest(TritonFieldModel):
name: Optional[str] = Field(None, json_schema_extra={'data_type': "TYPE_STRING"})
subname: Optional[str] = Field(None, json_schema_extra={'data_type': "TYPE_STRING"})
biznum: Optional[str] = Field(None, json_schema_extra={'data_type': "TYPE_STRING"})
address: Optional[List[str]] = Field(None, json_schema_extra={'data_type': "TYPE_STRING"})
tel: Optional[List[str]] = Field(None, json_schema_extra={'data_type': "TYPE_STRING"})
@root_validator(pre=True)
def check_all_fields_empty(cls, values):
if not any(bool(v) for v in values.values()):
raise ValidationError("All fields cannot be empty", model=cls.__name__)
모델 별 IO type conversion 모듈화
각 모델의 입출력 데이터 변환 과정을 캡슐화하고, pb_tensor
↔ pydantic 간 변환 작업을 공통 함수로 묶어 Base Triton Python 모델에서 활용하도록 설계하였습니다. 이를 통해 개발자는 데이터 변환의 내부 동작을 신경 쓸 필요 없이, 일관된 방식으로 모델을 호출할 수 있습니다.
아래는 구현 예시입니다. 이 함수는 Pydantic Request 객체를 받아 Triton pb_tensor
로 변환하고, 모델 추론 후 결과를 Pydantic Response 객체로 반환합니다.
def _infer_model(self, request_model, response_model_cls, model_name, request_model, **infer_kwargs):
# Converts Pydantic Request to Triton pb_tensors.
pb_tensors = self.convert_pydantic_to_pb_tensors(request_model, batch_inference)
# Runs model inference.
infer_request = pb_utils.InferenceRequest(
model_name=model_name,
inputs=pb_tensors,
requested_output_names=response_model_cls.get_field_names(),
**infer_kwargs,
)
infer_response = infer_request.exec()
# Converts Triton Response(pb_tensors) to Pydantic Response.
return self.convert_pb_tensors_to_pydantic(response, response_model_cls)
아래는 _infer_model
을 활용하여 모델을 호출하는 예시입니다. 개발자는 GeneratorRequest
와 GeneratorResponse
클래스만 선언해두고, 데이터 변환이나 모델 호출의 복잡한 과정을 신경 쓸 필요가 없습니다.
def infer_generator(self, request, text_input, max_tokens):
response_model_cls = schema.GeneratorResponse
request_model = schema.GeneratorRequest(text_input=text_input, max_tokens=max_tokens)
return self._infer_model(
request=request,
model_name="generator_bls",
request_model=request_model,
response_model_cls=response_model_cls,
)
BLS business logic 모듈화 및 testability 향상
BLS에 모여있던 비즈니스 로직과 전후처리 코드를 별도의 모듈로 분리하여 모듈의 결합도를 낮추는 방향으로 구조를 개선하였습니다. 이를 통해 코드 복잡도를 줄이고, 독립적으로 테스트와 유지보수가 가능해졌습니다.
- 전후처리 모듈화 및 유닛 테스트 도입:
- 학습 시 사용하는 비즈니스 로직과 전후처리 코드를 재사용할 수 있도록 별도의 모듈로 분리하였습니다.
- Triton Runtime 없이도 Python Runtime에서 독립적으로 실행 가능하도록 설계해, 유닛 테스트를 통해 각 모델의 전후처리 검증이 가능해졌습니다.
- BLS의 역할 재정의:
- BLS는 모델 호출과 E2E 검증 책임만을 담당하게 되었습니다. 이러한 설계는 시스템의 확장성을 보장하며, 새로운 요구사항이 추가되더라도 BLS 코드에 영향을 최소화할 수 있습니다.
- CI 도입:
- 전후처리 및 비즈니스 로직에 대해 수정여부에 따른 CI 테스트 파이프라인을 구축하였습니다.
- 이로써, 학습 과정에서 수정된 로직의 변경 사항이 서빙에 영향을 미치지 않도록 신속히 확인할 수 있습니다.
이러한 조치는 데이터 유효성 검증 강화, 코드 유지보수성 개선, 그리고 개발 생산성 향상이라는 목표를 효과적으로 달성하며 Triton 기반 LLM 서빙 개발 과정에서 생산성이 크게 높아졌습니다.
요약
NAVER Place는 NVIDIA TensorRT-LLM을 사용하여 LLM 엔진을 성공적으로 최적화하고 NVIDIA Triton Inference Server의 사용성을 개선했습니다. 이번 최적화를 통해 팀은 GPU 활용도를 극대화하여 전체 시스템 효율성을 더욱 향상시켰습니다. 이 전체 과정은 여러 SLM 기반 vertical services를 최적화하는 데 기여하여 NAVER Place를 보다 사용자 친화적으로 만들었습니다. 이 경험을 바탕으로 앞으로도 추가적인 vertical 모델을 개발하여 서비스에 적용할 예정입니다.