LLM 추론 속도를 극대화하는 고급 그래프 최적화 기법 총정리. FP16, INT8, 커널 퓨전 등 실전 적용 전략을 사례와 함께 소개합니다.
이전 글에서는 Computation Graph의 개념과 기초 최적화 기법에 대해 살펴봤습니다. 이번 글은 그에 이은 두 번째 글로, 보다 실전적인 최적화 전략을 담고 있습니다.
Computation graph 최적화는 추론 속도를 높이고 자원 효율성을 극대화하는 데 있어 핵심적인 역할을 합니다.
이를 위해 다양한 최적화 프레임워크가 등장했으며, 각 프레임워크는 서로 다른 graph 표현 방식, 최적화 전략, 실행 backend를 기반으로 동작합니다.
각 프레임워크의 적용 시점은 최적화 방식과 대응 가능한 환경에 큰 영향을 미칩니다.
예를 들어 TensorRT는 build-time 최적화에 특화되어 있는 반면, TorchDynamo나 ONNXRuntime EP는 runtime 시점에서도 유연한 graph 변형이 가능합니다.
실제 서비스에 적용할 경우, 이들 프레임워크의 조합을 통해 최적화 범위를 확장하는 것도 하나의 전략이 될 수 있습니다.
예를 들어 PyTorch → ONNX export → TensorRT 빌드까지의 파이프라인을 구성하면, 학습 환경의 유연성과 추론 환경의 성능을 모두 확보할 수 있습니다.
TorchScript는 PyTorch 모델을 static computation graph로 변환하여 Python 없이 추론 가능한 형태로 만드는 중간 표현입니다.
Triton Inference Server에서 PyTorch 모델을 배포할 때 가장 널리 사용되는 표준 포맷이기도 합니다.
4.2.1 Graph 구조 및 동작 방식
TorchScript는 PyTorch 모델을 ScriptModule 또는 TracedModule로 변환해 static 연산 그래프 형태로 표현합니다.
내부적으로는 SSA(Static Single Assignment) 기반의 intermediate representation을 사용하며,
모델의 연산, tensor 흐름, 조건문 등을 모두 graph node로 변환합니다.
변환 방식은 두 가지입니다.
torch.jit.trace
torch.jit.script
생성된 모델은 .pt
파일로 export 가능하며,
Python runtime 없이 C++(LibTorch) 혹은 Triton Server에서 직접 실행할 수 있습니다.
TorchScript는 static graph 상에서 기본적인 최적화를 자동으로 수행합니다.
최적화 강도는 보수적인 편이지만, Python-free 추론 환경 구성에 매우 적합합니다.
장점
.pt
포맷을 그대로 배포 가능
한계
trace
방식은 dynamic control flow를 반영하지 못함script
방식은 Python 문법 제약이 많음
TorchScript는 Triton Inference Server의 PyTorch backend에서
공식 지원하는 유일한 static graph 포맷입니다.
기본 적용 흐름
.eval()
전환torch.jit.trace()
또는 torch.jit.script()
로 graph export.pt
파일을 model_repository/my_model/1/
에 저장config.pbtxt
에서 backend를 pytorch_libtorch
로 지정model_repository/
└── my_model/
├── 1/
│ └── model.pt
└── config.pbtxt
# config.pbtxt 예시
name: "my_model"
platform: "pytorch_libtorch"
max_batch_size: 8
input [
{ name: "INPUT__0", data_type: TYPE_FP32, dims: [128] }
]
output [
{ name: "OUTPUT__0", data_type: TYPE_FP32, dims: [64] }
]
실전 활용 팁
optimize_for_inference()
→ Dropout 제거, Folding 적용
# Tracing 기반 TorchScript export
import torch
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(128, 64)
self.norm = nn.LayerNorm(64)
self.act = nn.GELU()
def forward(self, x):
return self.norm(self.act(self.linear(x)))
model = MyModel().eval()
example_input = torch.randn(1, 128)
traced_model = torch.jit.trace(model, example_input)
traced_model.save("model.pt")
# Scripting 기반 TorchScript export
class MyScriptedModel(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(128, 64)
self.dropout = nn.Dropout(0.1)
def forward(self, x):
if x.mean() > 0:
return self.linear(x)
return self.dropout(x)
model = MyScriptedModel().eval()
scripted_model = torch.jit.script(model)
scripted_model.save("scripted_model.pt")
# optimize_for_inference 적용
from torch.jit import optimize_for_inference
model = MyModel().eval()
example_input = torch.randn(1, 128)
ts_model = torch.jit.trace(model, example_input)
optimized_model = optimize_for_inference(ts_model)
optimized_model.save("optimized_model.pt")
# FP16 tracing export (AMP 기반)
from torch.cuda.amp import autocast
model = MyModel().eval().cuda()
example_input = torch.randn(1, 128).cuda()
with autocast(dtype=torch.float16):
traced_model = torch.jit.trace(model, example_input)
traced_model.save("model_fp16.pt")
FP16 추론을 Triton에서 사용하려면 config.pbtxt
의 input/output
타입을 TYPE_FP16
으로 설정해야 합니다.
ONNX(Open Neural Network Exchange)는 PyTorch, TensorFlow, MXNet 등 다양한 프레임워크에서
학습된 모델을 하나의 공통 포맷으로 export하고 추론 환경에 최적화할 수 있도록 설계된 중간 표현(IR)입니다.
ONNX는 추론 중심의 static graph 기반 구조를 사용하며,
ONNX Runtime, TensorRT, OpenVINO 등 다양한 backend에서 동일한 모델을 실행할 수 있습니다.
ONNX는 static computation graph 형식으로 모델을 표현합니다.
모델 구조와 weight는 export 시점에 완전히 고정되며, Python 없이 실행 가능한 형태로 변환됩니다.
PyTorch에서는 torch.onnx.export()
를 통해 ONNX 모델을 생성하며,
내부적으로 tracing 방식으로 연산 그래프를 기록합니다.
생성된 .onnx
파일은 ONNX IR(GraphProto) 형식이며,
ONNX Runtime, TensorRT, OpenVINO 등 다양한 추론 엔진에서 바로 로드해 실행할 수 있습니다.
ONNX는 static graph 기반이기 때문에 다양한 최적화 기법을 적용할 수 있습니다.
ONNX 자체 optimizer 외에도 ONNX Runtime, TensorRT, GraphSurgeon 등에서 backend-specific 최적화가 추가로 적용됩니다.
장점
한계
ONNX는 Triton Inference Server에서 가장 널리 사용되는 모델 포맷 중 하나입니다. onnxruntime
또는 tensorrt
backend를 통해 GPU/CPU에서 추론을 실행할 수 있습니다.
실전 적용 흐름
torch.onnx.export()
로 ONNX 모델 생성model_repository/my_model/1/model.onnx
에 저장config.pbtxt
에서 backend를 onnxruntime
또는 tensorrt
로 설정
Backend 선택 가이드
추가 최적화 도구
onnxsim
– 불필요한 노드 제거 및 shape 고정onnxruntime_tools
, onnx-graphsurgeon
– 수동 최적화
# 기본 ONNX export
import torch
import torch.nn as nn
class MyModel(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(128, 64)
def forward(self, x):
return self.fc(x)
model = MyModel().eval()
dummy_input = torch.randn(1, 128)
torch.onnx.export(
model,
dummy_input,
"model.onnx",
opset_version=13,
input_names=["input"],
output_names=["output"]
)
# dynamic_axes 지정 (batch 크기 유동적일 때)
torch.onnx.export(
model,
dummy_input,
"model_dynamic.onnx",
opset_version=13,
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"},
"output": {0: "batch_size"}
}
)
# 학습 그래프 그대로 export (e.g., weight 미포함, constant folding 제거)
torch.onnx.export(
model,
dummy_input,
"model_raw.onnx",
export_params=False,
do_constant_folding=False,
training=torch.onnx.TrainingMode.TRAINING
)
# 설치: pip install onnxsim
import onnx
from onnxsim import simplify
model = onnx.load("model_dynamic.onnx")
model_simp, check = simplify(model)
assert check
onnx.save(model_simp, "model_simplified.onnx")
# model_repository/my_model/config.pbtxt
name: "my_model"
platform: "onnxruntime_onnx"
max_batch_size: 16
input [
{ name: "input", data_type: TYPE_FP32, dims: [128] }
]
output [
{ name: "output", data_type: TYPE_FP32, dims: [64] }
]
instance_group [
{ kind: KIND_GPU, count: 1 }
]
# Calibration용 데이터 리더
from onnxruntime.quantization import CalibrationDataReader
import numpy as np
class MyCalibDataReader(CalibrationDataReader):
def __init__(self):
self.data_iter = iter([
{"input": np.random.randn(1, 128).astype(np.float32)}
for _ in range(100)
])
def get_next(self):
return next(self.data_iter, None)
# Static Quantization 수행
from onnxruntime.quantization import quantize_static, QuantType
quantize_static(
model_input="model_simplified.onnx",
model_output="model_int8.onnx",
calibration_data_reader=MyCalibDataReader(),
quant_format="QDQ",
activation_type=QuantType.QUInt8,
weight_type=QuantType.QInt8,
optimize_model=True
)
# INT8 Triton config 예시 (TensorRT backend)
name: "my_model_int8"
platform: "tensorrt_plan"
max_batch_size: 16
input [
{ name: "input", data_type: TYPE_FP32, dims: [128] }
]
output [
{ name: "output", data_type: TYPE_FP32, dims: [64] }
]
instance_group [
{ kind: KIND_GPU, count: 1 }
]
precision_mode: INT8
TensorRT는 NVIDIA에서 제공하는 고성능 딥러닝 추론 최적화 라이브러리입니다.
FP16, INT8, layer fusion, kernel auto-tuning 등의 최적화를 통해 GPU 상에서 최소 latency, 최대 throughput을 달성하는 것이 목적입니다.
PyTorch, ONNX, TensorFlow 모델을 입력으로 받아
CUDA-compatible static inference engine (.plan
파일)으로 변환하여 추론을 수행합니다.
TensorRT는 모델 최적화를 다음과 같은 세 단계로 구성합니다:
.plan
파일 생성
TensorRT는 다음과 같은 고수준 최적화 패스를 지원합니다.
TensorRT는 .plan
파일 형식의 static engine을 생성합니다.
이 파일은 최적화된 CUDA kernel, memory binding, shape profile을 포함하며, 추론 시 바로 실행할 수 있습니다.
Engine 설정 주요 항목
max_batch_size
: 최대 배치 크기min/opt/max_shape
: dynamic shape profile 정의precision_mode
: FP16, INT8 설정workspace_size
: GPU 메모리 사용 한도Python, C++, CLI(trtexec
) 등 다양한 방법으로 엔진을 생성할 수 있습니다.
Triton에서 TensorRT .plan
파일을 사용하려면 platform: "tensorrt_plan"
을 지정합니다.
실전 적용 흐름
.plan
생성model_repository/my_model/1/model.plan
에 저장config.pbtxt
에 precision, shape profile 등 설정
INT8 사용 시
calib.cache
) 파일을 함께 제공하면 재학습 없이 deployment 가능
장점
한계
# ONNX → TensorRT 엔진 변환 (Python API 사용)
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit # CUDA context 초기화
import numpy as np
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
onnx_path = "model.onnx"
engine_path = "model.plan"
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, TRT_LOGGER)
with open(onnx_path, "rb") as f:
assert parser.parse(f.read())
config = builder.create_builder_config()
config.max_workspace_size = 1 << 30 # 1GB
config.set_flag(trt.BuilderFlag.FP16)
# Dynamic shape profile 설정
profile = builder.create_optimization_profile()
profile.set_shape("input", (1, 128), (4, 128), (16, 128))
config.add_optimization_profile(profile)
engine = builder.build_engine(network, config)
with open(engine_path, "wb") as f:
f.write(engine.serialize())
# 사용자 정의 INT8 Calibrator
import tensorrt as trt
import numpy as np
import pycuda.driver as cuda
class MyEntropyCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, cache_file="calib.cache"):
super().__init__()
self.data = [np.random.randn(4, 128).astype(np.float32) for _ in range(50)]
self.device_input = cuda.mem_alloc(self.data[0].nbytes)
self.cache_file = cache_file
self.index = 0
def get_batch_size(self):
return 4
def get_batch(self, names):
if self.index >= len(self.data):
return None
batch = self.data[self.index]
cuda.memcpy_htod(self.device_input, batch)
self.index += 1
return [int(self.device_input)]
def read_calibration_cache(self):
try:
with open(self.cache_file, "rb") as f:
return f.read()
except:
return None
def write_calibration_cache(self, cache):
with open(self.cache_file, "wb") as f:
f.write(cache)
name: "bert_int8_trt"
platform: "tensorrt_plan"
max_batch_size: 16
input [
{ name: "input", data_type: TYPE_FP32, dims: [128] }
]
output [
{ name: "output", data_type: TYPE_FP32, dims: [64] }
]
instance_group [
{ kind: KIND_GPU, count: 1 }
]
dynamic_batching {
max_queue_delay_microseconds: 100
preferred_batch_size: [4, 8, 16]
}
optimization {
execution_accelerators {
gpu_execution_accelerator : [ "tensorrt" ]
}
}
parameters: {
key: "precision_mode"
value: {
string_value: "INT8"
}
}
# 기본 FP16 변환 (batch=1)
trtexec \
--onnx=bert_onnx/model_simp.onnx \
--saveEngine=bert_fp16.plan \
--explicitBatch \
--fp16 \
--minShapes=input:1x128 \
--optShapes=input:4x128 \
--maxShapes=input:16x128 \
--workspace=2048
Transformer 기반 임베딩 시스템에서 실시간 추론 속도 개선을 위해 하드웨어 교체, 정밀도 축소, 그래프 최적화를 적용했습니다.
Token 길이에 따른 latency 구성 요소를 분석하고, 각 최적화 단계가 실제 성능에 미치는 영향을 정량적으로 보고합니다.
FP16 기반 정밀도 축소
Computation Graph 최적화
Baseline(비 최적화)은 Fixed Length 방식으로 항상 max_token_length의 지연시간을 요구했으나, Variable Length를 지원하면서 적은 토큰 수에서 latency가 수백 배 이상 개선되었습니다.
특히 runtime latency가 중요한 Query의 경우 토큰 수가 적기 때문에 향상 폭이 매우 큰 것이 핵심입니다. Graph 최적화를 통해 Async Context Latency도 수십 배 이상 개선되었습니다.
고객사 데이터를 대상으로 Reranker 모델의 속도를 static graph 최적화를 통해 개선한 실험 결과입니다.
정확도는 거의 동일하거나 유지되었으며, 평균적으로 1.33배 빠른 latency 개선이 확인되었습니다.
대부분 케이스에서 정확도 유지 혹은 소폭 향상
평균적으로 latency 1.33배 개선
특히 300ms 이상 응답 시간이 발생하는 기업 환경에서 체감 성능 개선 효과가 명확
Precision 관련
Memory 관리
배포 측면
저희가 자체 보유한 산업 도메인별 데이터를 기반으로 다양한 embedding 모델의 성능을 비교한 결과입니다.
모델 설명
주요 결과
결론 및 도입 효과
이러한 성능-속도-비용 균형은 실시간 질의 대응이 중요한 산업 현장에서 중요한 경쟁력으로 작용하며,
특히 1024 token 이상의 긴 문서 처리에 있어서도 유리한 응답성을 확보할 수 있습니다.
RAG 시스템의 실시간 응답 성능은 결국 Transformer 기반 모델의 추론 효율성에 의해 좌우됩니다. 특히 cross-encoder 기반의 reranker는 문서 수에 따라 연산량이 선형 증가하기 때문에, per-query latency의 병목 지점으로 작용하기 쉽습니다.
이번 프로젝트에서는 PyTorch 기반 모델을 static computation graph로 변환하고, 연산 구조, 데이터 흐름, 메모리, 정밀도, Transformer 구조 특화 등 다각도의 최적화 기법을 체계적으로 적용해보았습니다.
정적 최적화: 구조에서 출발한 변화
Linear + Add + LayerNorm을 하나의 fused kernel로 통합하거나, Q/K/V projection을 하나의 연산으로 병합하는 Operator Fusion은 kernel 호출 횟수를 줄이고 launch overhead를 줄여주는 핵심 기법이었습니다.
또한 Transpose Elimination, Common Subexpression Elimination, Dead Code Elimination 등 그래프 수준의 불필요한 흐름을 정리하면서 중간 tensor 이동을 줄이고, 더 나은 메모리 재사용 패턴을 확보할 수 있었습니다.
이와 함께 FP16 기반 정밀도 축소를 통해 전체 tensor 크기를 줄이고 Tensor Core를 활용한 연산 속도 향상을 달성했습니다. 단, LayerNorm이나 Softmax처럼 수치적으로 민감한 연산은 FP32로 유지하여 안정성도 확보했습니다.
Transformer에 특화된 최적화도 핵심
Transformer 모델은 그 자체로 반복 구조가 뚜렷한 모듈형 설계이기 때문에, Fused Multi-Head Attention, Rotary Embedding Precomputation, Key/Value Caching 같은 특화된 최적화 기법이 매우 효과적으로 작동합니다. 이들 기법은 특히 long-context inference 환경에서 memory reuse와 latency를 동시에 개선할 수 있는 핵심 도구입니다.
Framework 조합으로 실전 성능 극대화
모델 최적화는 결국 어떤 프레임워크 조합을 쓰느냐에 따라 달라집니다.
특히 ONNX → TensorRT 경로를 활용하면 static graph 기반 구조에서 kernel-level 최적화와 precision tuning을 모두 적용할 수 있어, 엔터프라이즈 환경에서도 실용성과 효율성을 동시에 만족할 수 있습니다.
실험으로 확인된 효과
4096 token 기준:
즉, 10배 이상의 latency 개선을 확인했습니다.
또한 OpenAI Embedding API와 비교해도 최대 5배 빠른 응답 속도를 확보했습니다.
모델 최적화는 단순한 연산 속도 향상을 넘어, 실환경에서의 응답 성능과 안정성 확보를 위한 중요한 과정입니다. 이번 프로젝트를 통해 우리는 static computation graph 기반의 다양한 최적화 기법이 Transformer 모델의 추론 효율을 실질적으로 개선할 수 있음을 확인했습니다.
아직도 개선할 여지는 많습니다. 하지만 이러한 시도를 통해 점진적으로 시스템을 최적화해 나간다면, 실시간 RAG 시스템의 품질과 사용자 경험 모두를 향상시킬 수 있으리라 기대합니다.