Blogs & Articles
>
Computation Graph Optimization 1부 - Computation Graph, 최적화의 출발점
Blog
May 28, 2025

Computation Graph Optimization 1부 - Computation Graph, 최적화의 출발점

실시간 RAG 시스템의 추론 병목을 해결하기 위한 Computation Graph Optimization 기법을 정리했습니다. Operator Fusion부터 Memory 최적화까지, 실전 적용 가능한 핵심 전략을 소개합니다.

최근 Transformer 기반 모델의 실시간 추론 효율이 중요한 이슈로 떠오르고 있습니다. 이에 따라 Computation Graph 최적화에 대한 관심도 높아지고 있는데요, 이번 글에서는 이 주제를 2편에 걸쳐 자세히 다뤄보려 합니다.

  • 이번 Part 1에서는 Computation Graph의 개념과 연산자 및 데이터 흐름 최적화 기법을 소개합니다.
  • 이어지는 Part 2에서는 정밀도 축소, 실행 그래프 및 구조 특화 최적화 기법 등 심화 내용을 다룰 예정입니다.
  • 이 글은 올거나이즈 RAG팀의 조한준 엔지니어님이 작성하셨습니다.

1. 서론

1.1. 배경

Transformer 기반 모델은 고정밀 matrix 연산과 반복적인 self-attention 구조를 포함하고 있어, 추론 과정에서 latency가 크게 발생합니다. Retrieval-augmented generation (RAG) 시스템은 사용자 질의에 대한 빠른 응답이 요구되기 때문에, 이러한 추론 지연은 전체 응답 품질에 직접적인 영향을 미치게 됩니다.

대부분의 모델은 PyTorch 기반으로 구현되어 있으며, dynamic computation graph 구조를 사용합니다. 이 구조는 개발 단계에서는 유연성을 제공하지만, 추론 성능 최적화 측면에서는 비효율적인 측면이 존재합니다. 실시간 처리가 중요한 RAG 환경에서는 이러한 부분이 성능 병목으로 이어질 수 있습니다.

1.2. 필요성

Initial Retrieval (Bi-Encoder)와 Re-ranking (Cross-Encoder)의 추론 시간이 RAG에서 주요 병목으로 작용

RAG 파이프라인에서 추론 지연이 집중되는 구간은 크게 두 가지입니다. 하나는 query를 encoding하는 bi-encoder 단계이며, 다른 하나는 candidate 문서를 재정렬하는 cross-encoder 단계입니다.

특히 cross-encoder는 query와 document를 함께 인코딩하는 구조이기 때문에 계산량이 많고, 후보 문서 수에 따라 추론 시간이 선형적으로 증가합니다. 따라서 per-query latency를 줄이기 위한 구조적 최적화가 필수적이며, 이를 위해 static graph 변환과 compiler 수준의 최적화 적용이 요구됩니다.

1.3. 최적화 목표

Development vs Deployment계산 그래프 최적화를 통해서 실시간 모델의 추론속도를 개선할 수 있음

RAG 시스템에서의 실시간 처리를 위해 다음과 같은 최적화 목표가 설정됩니다:

  • Latency: 실시간 응답 요구를 충족하기 위해 inference time을 수십 밀리초 이내로 단축
  • Throughput: 제한된 자원 내에서 초당 처리 가능한 질의 수(QPS)를 극대화

이를 위해 적용 가능한 최적화 기법으로는 operator fusion, constant folding, precision reduction(FP16/INT8), memory planning 등이 있습니다.

또한 이러한 최적화를 실현하기 위해 TorchScript(JIT 기반 정적 변환), ONNX(중간 표현 기반 최적화), TensorRT(NVIDIA GPU 환경에 최적화된 고성능 추론 엔진)와 같은 프레임워크가 활용됩니다.

2. 배경

2.1. Computation Graph 개념

딥러닝 모델은 연산과 데이터 흐름을 Directed Acyclic Graph(DAG) 형태로 표현할 수 있습니다. 그래프의 노드는 연산자(operator), 엣지는 텐서(tensor)의 흐름을 나타냅니다.

학습(training) 과정에서는 autograd 엔진이 동적 그래프(dynamic graph)를 사용하여 gradient를 계산합니다. 반면, 추론(inference) 단계에서는 동일한 연산이 반복되므로, 정적 구조(static graph)로 변환할 경우 최적화에 유리합니다.

PyTorch는 기본적으로 dynamic graph 방식을 채택하고 있지만, TorchScript나 ONNX를 통해 모델을 export하면 static graph로 변환할 수 있습니다.

2.2. Transformer 연산 구조와 추론 비용

Transformer 모델은 self-attention, feed-forward network(FFN), residual connection, layer normalization 등의 블록으로 구성됩니다.

이들 블록에서 수행되는 주요 연산으로는 matrix multiplication(MatMul), softmax, GELU, normalization 등이 있으며, 이들은 대부분 높은 메모리 대역폭과 연산량을 요구합니다. 특히 attention 연산은 시퀀스 길이 n에 대해 O(n²)의 시간복잡도를 가집니다.

encoder-only 모델(BERT)은 입력 전체를 한 번에 인코딩하지만, decoder-only 모델(GPT 계열)은 auto-regressive 방식으로 토큰 단위의 반복 연산을 수행하게 됩니다. 이로 인해 inference 단계에서 latency가 누적되는 구조적 한계가 존재합니다.

2.3. Static Graph 변환의 이점

연산 그래프를 static 형태로 고정하면 전체 연산 흐름을 사전에 분석할 수 있어 다양한 최적화가 가능합니다. 대표적인 최적화 기법으로는 다음과 같습니다:

  • constant folding
  • operator fusion
  • dead code elimination
  • memory reuse

또한 반복되는 구조를 unroll하거나 입력 shape이 고정된 경우 shape inference를 통해 memory allocation을 정적으로 구성할 수 있습니다.

dynamic graph는 유연성 면에서는 장점이 있지만, Python 인터프리터의 오버헤드와 GIL(Global Interpreter Lock)로 인해 최적화 여지가 제한적입니다. 반면 static graph로 변환하면 Python-free 추론이 가능해지며, multithreaded 실행과 low-level inference engine(TensorRT 등)에도 적합한 구조를 구성할 수 있습니다.

2.4. RAG 시스템에서의 추론 시점과 요구 조건

RAG 시스템에서의 추론은 다음과 같은 두 시점에서 발생합니다:

  • Async Document Encoding: 문서 임베딩을 사전 생성하여 저장하는 단계 (bi-encoder)
  • Runtime Query Processing & Reranking: 사용자의 질의에 따라 실시간으로 encoding 및 재정렬을 수행하는 단계 (bi-encoder + cross-encoder)

특히 cross-encoder는 질의와 각 문서를 결합해 개별 추론을 수행하므로, 문서 수가 증가할수록 추론 지연이 선형적으로 누적됩니다.

실시간 검색 환경에서는 일반적으로 300~500ms 이내의 지연이 요구되며, 이 범위를 초과할 경우 사용자 경험에 직접적인 영향을 미치게 됩니다. 따라서 미세한 수준의 연산 최적화도 운영 품질 측면에서 매우 중요합니다.

3. Computation Graph Optimization Techniques

Computation graph는 모델 추론 시 수행되는 연산들의 정적 실행 흐름을 정의하며, latency 및 throughput 개선을 위한 최적화의 핵심 대상입니다. 특히 Transformer 기반 모델에서는 self-attention과 projection 계층 등의 반복적인 연산 구조로 인해, 효율적인 그래프 최적화가 실시간 응답 성능 향상에 직결됩니다.

정적 그래프(static graph)를 기반으로 한 최적화는 연산 구조의 간소화, 메모리 접근 감소, 실행 순서 개선, 연산 정밀도 변경 등을 통해 추론 효율을 높일 수 있습니다. 이러한 최적화는 단일 연산자 수준이 아니라 subgraph 단위로 적용될 때 latency 개선 효과가 더욱 큽니다.

Computation graph 최적화 기법은 일반적으로 다음의 다섯 범주로 분류할 수 있습니다:

1. 연산 구조 최적화 (Operator-level Optimization)

  • Operator Fusion: 연속된 연산들을 하나의 커스텀 커널로 결합하여 kernel launch overhead를 줄이고 memory access를 최적화합니다.
  • Constant Folding: 그래프 내 상수 값을 컴파일 시점에 미리 계산해 실행 시 연산량을 줄입니다.
  • Dead Code Elimination: 결과에 영향을 주지 않는 연산자나 경로를 제거합니다.

2. 데이터 흐름 최적화 (Dataflow Optimization)

  • Transpose 제거: 연산 순서를 조정하여 불필요한 transpose 연산을 제거하거나 병합합니다.
  • Shape Inference: 입력 shape이 고정된 경우, 그래프 내 모든 shape을 사전 계산하여 동적 계산을 제거합니다.
  • Common Subexpression Elimination (CSE): 반복되는 서브그래프를 재사용하도록 통합합니다.

3. 메모리 및 자원 최적화 (Memory & Resource Optimization)

  • Memory Planning: 연산 순서에 따라 메모리 재사용 계획을 수립하여 peak memory usage를 줄입니다.
  • In-place Operation: 중간 텐서를 재사용하여 메모리 할당을 최소화합니다.

4. 정밀도 축소 (Precision Reduction)

  • FP16/INT8 Quantization: 연산을 float16 또는 int8로 수행하여 메모리 대역폭을 줄이고 처리 속도를 높입니다. 모델 정확도에 영향을 줄 수 있으나, 적절한 calibration 기법을 통해 안정화할 수 있습니다.

5. Transformer 특화 최적화

  • Fused Attention: self-attention 블록을 하나의 fused kernel로 구성해 latency를 대폭 줄입니다.
  • Static Rotary Embedding: Rotary positional embedding을 미리 계산하여 그래프에 고정합니다.
  • Bias 및 LayerNorm Folding: bias 추가 및 정규화 연산을 선/후행 연산과 결합하여 효율화합니다.

이러한 최적화 기법은 정확도 손실 없이 latency를 줄이는 conservative 방식부터, precision trade-off를 수용해 extreme throughput을 추구하는 aggressive 방식까지 다양하게 활용됩니다.

대부분의 기법은 static graph를 전제로 적용되며, ONNX, TorchScript, TensorRT 등의 inference backend에서도 공통적으로 지원되거나 유사한 형태로 구현 가능합니다.

3.1 Operator-Level Optimization

Computation graph 최적화의 첫 단계는 연산자 수준에서 불필요한 연산을 제거하거나, 연산을 합쳐 효율적으로 실행하는 것입니다.  
이 과정은 대부분 static graph 상에서 정적으로 분석되며, 특히 Transformer처럼 반복되는 연산 패턴이 많은 구조에서는 subgraph 단위 최적화가 latency를 줄이는 데 매우 효과적입니다.

대표적으로 많이 활용되는 연산자 최적화 기법은 다음 세 가지입니다: Operator Fusion, Constant Folding, Dead Code Elimination

3.1.1 Operator Fusion

Operator Fusion은 연속된 연산자들을 하나의 fused kernel로 합쳐 실행하는 기법입니다.  
이렇게 하면 연산 간 메모리 이동을 줄이고 kernel 호출 수를 줄일 수 있어 GPU 자원을 보다 효율적으로 쓸 수 있습니다.

Transformer 구조에서는 Linear → Add → LayerNorm이나, Q/K/V projection → Concat → Transpose 같은 연산 흐름이 반복적으로 등장합니다.  
이들을 각각 따로 실행하면 연산량도 많고 메모리 접근도 자주 발생해 비효율적입니다.  

이런 경우, 연산들을 하나로 묶어 FusedLinearNorm 또는 FusedQKVProjection 형태의 단일 커널로 최적화할 수 있습니다.  
이 최적화는 TorchScript, ONNX Runtime, TensorRT 같은 백엔드에서 자동으로 적용되는 경우도 많습니다.

다만 모든 구조에 적용할 수 있는 건 아니고,  아래 조건에서만 안정적으로 적용됩니다.

  • 연산 간 데이터 흐름이 선형적이고  
  • 텐서의 shape이 고정되어 있으며  
  • control flow나 branching이 없는 경우  

3.1.2 Constant Folding

Constant Folding은 계산 결과가 항상 일정한 연산을 미리 계산해두고, runtime에서 다시 계산하지 않도록 하는 기법입니다.

Transformer 구조에서는 rotary embedding, attention mask, normalization 초기값처럼 반복 수행되지만 변하지 않는 연산들이 많습니다.  
이러한 연산을 export 시점에 미리 계산해서 static tensor로 바꿔두면, runtime에서 별도 연산 없이 indexing만으로 처리할 수 있습니다.

예를 들어 rotary embedding의 경우, sin/cos 값을 토큰마다 계속 계산하는 대신, export 단계에서 미리 만들어 넣어두면 추론 시에는 lookup만 수행하면 됩니다.

이 기법은 전체 latency를 획기적으로 줄이기보다는, 불필요한 연산을 제거하고 그래프를 단순화해 latency를 안정화하는 데 효과적입니다.

3.1.3 Dead Code Elimination (DCE)

Dead Code Elimination (DCE)은 추론 시점에 실제로 사용되지 않는 연산을 그래프에서 완전히 제거하는 작업입니다.

보통 model.eval()을 사용하면 Dropout이나 BatchNorm 같은 연산이 runtime에서 "비활성화"되긴 하지만, 연산자 자체는 여전히 그래프에 남아 있는 경우가 많습니다.  
DCE는 이처럼 사용되지 않는 연산자와 노드를 아예 제거하여, memory 사용량과 kernel 호출을 줄여줍니다.

예를 들어 Dropout은 학습 시에는 필요하지만 추론에서는 무의미합니다.  
그런데 이 연산이 그래프 안에 남아 있으면, 메모리도 차지하고 실행 오버헤드도 생깁니다.  
그래서 ONNX export 시점에서 Dropout 연산 자체를 제거해 그래프를 정리하면, 훨씬 효율적인 추론이 가능해집니다.

3.1.4 Operator-Level Optimization 정리표

최적화 기법 대표 적용 예시 기대 효과
Operator Fusion Linear + Add + LayerNorm,
Q/K/V + Transpose
kernel 수 감소, memory access 최적화
Constant Folding sin/cos 계산,
attention mask 사전 계산
runtime 연산 제거, latency 안정화
Dead Code Elimination Dropout, Loss,
backward 연산 제거
불필요 노드 제거, 메모리 사용량 절감

3.1.5 Operator-Level Optimization 예시

[예시 3.1.1] FusedLinearNorm

문제  
Transformer block에서는 다음과 같은 연산이 순차적으로 수행됩니다:  
x → Linear(W) → Add(bias) → Add(residual) → LayerNorm(γ, β)

이 연산 흐름은 개별 연산자가 각각 별도의 kernel로 실행되며, 중간 결과가 GPU 메모리에 반복 저장됩니다.  
특히 Layer 수가 많을수록 memory access cost와 kernel launch overhead가 누적됩니다.

해결  
이러한 연산 흐름은 하나의 fused kernel로 병합하여 처리할 수 있습니다.  
일반적으로 FusedLinearNorm이라는 연산자 형태로 구현되며, 다음과 같은 구조로 합쳐집니다:  
y = LayerNorm(Linear(x) + bias + residual)

이 최적화는 TorchScript, ONNX Runtime, TensorRT 등에서 연속된 연산 패턴을 정적으로 감지한 뒤 자동으로 적용됩니다.

적용 효과  

  • 중간 tensor 생성 제거로 메모리 접근 최소화  
  • kernel 호출 수 감소로 실행 효율 향상  
  • 반복 구조에서 누적되는 latency 비용 절감

[예시 3.1.2] FusedQKVProjection

문제  
Self-attention에서는 입력 x에 대해 다음과 같은 연산이 수행됩니다:  
x → Linear(W_q), Linear(W_k), Linear(W_v) → concat → transpose

이는 3개의 Linear 연산과 그 뒤를 잇는 concat 및 transpose 연산으로 나뉘며, 각 단계마다 별도의 memory copy 및 kernel call이 발생합니다.

해결  
3개의 Linear 연산을 하나의 weight 행렬 W_qkv로 합쳐서 projection을 한 번에 처리하고,  
concat 및 transpose 연산도 포함한 fused kernel을 적용할 수 있습니다.  

TensorRT의 MHA(Multi-head Attention) Plugin이나 HuggingFace Optimum의 ONNX 패턴 최적화 시에도 이 방식이 활용됩니다.

적용 효과  

  • Linear 연산이 3회 → 1회로 축소  
  • concat + transpose까지 단일 연산자 내부 처리 가능  
  • attention 준비 단계의 연산 효율 개선

[예시 3.1.3] PrecomputedRotaryEmbedding

문제  
Rotary positional embedding은 다음과 같은 연산을 반복 수행합니다:  
positional angle θ → sin(θ), cos(θ) → elementwise embedding 곱

이 과정은 매 토큰마다 sin, cos를 계산하고 embedding 벡터에 적용하기 때문에,  
시퀀스 길이가 길어질수록 trigonometric 함수 호출로 인한 latency가 증가합니다.

해결  
Rotary embedding에 사용되는 sin, cos 값을 모델 export 시점에 미리 계산한 고정된 tensor로 삽입합니다.  
이렇게 하면 runtime에는 단순히 indexing만 수행하게 되어, 연산 비용이 크게 줄어듭니다.  
ONNX export 중 constant folding 단계에서 적용됩니다.

적용 효과  

  • sin/cos 연산 제거  
  • embedding 처리 구조를 정적으로 고정 가능  
  • decoder에서 토큰 단위 반복 연산의 안정화 효과

[예시 3.1.4] DropoutRemoval (Dead Code Elimination)

문제  
Dropout 연산은 학습 시에는 중요한 regularization 기법이지만,  
inference 시점에는 무의미한 연산입니다. 그럼에도 불구하고 Dropout 연산이 그래프에 남아 있다면,  
다음과 같은 구조가 유지됩니다:  
x → Linear → Dropout → Add → LayerNorm

이 경우 Dropout을 위한 memory buffer가 생성되고, 실행 경로도 유지되기 때문에 오버헤드가 발생합니다.

해결  
ONNX로 export 시, Dead Code Elimination(DCE) pass를 통해 Dropout 노드를 완전히 제거합니다.  
Dropout의 입력과 출력을 직접 연결하여, Dropout 연산 자체가 실행 그래프에서 제거됩니다.

적용 효과  

  • 불필요한 연산자 제거로 그래프 경량화  
  • memory allocation 감소  
  • 후속 최적화 및 scheduling 시 효율성 향상

3.2 Dataflow-Level Optimization

Dataflow-level 최적화는 연산 간 데이터 이동을 최소화하고, 중복된 연산을 제거하는 데 초점을 둡니다. 모델 구조를 바꾸는 것이 아니라, 연산이 흘러가는 경로를 최적화하는 방식이라고 이해하시면 됩니다.

Transformer 구조에서는 attention, residual, projection 등 다양한 블록에서 shape 변경, transpose, broadcast 연산이 자주 사용되는데,  이런 연산은 눈에 띄는 연산량은 적어도 memory bandwidth와 latency에는 큰 영향을 줍니다.

여기서는 세 가지 대표적인 최적화 기법을 소개합니다:  
Transpose Elimination,

Shape Inference,

Common Subexpression Elimination

3.2.1 Transpose Elimination

Transpose 연산은 연산량 자체는 작지만, memory layout을 바꾸기 때문에 memory 이동이 많고 GPU 캐시 효율이 떨어집니다. 특히 Transpose → Transpose처럼 inverse pair가 반복되는 경우, 의미 없는 연산만 늘어나게 됩니다.

예를 들어 다음과 같은 경우를 생각해볼 수 있습니다:

  • x → Transpose(A→B) → Transpose(B→A) → 결과적으로 아무 일도 안 일어난 것과 같음  
  • MatMul 앞뒤로 Transpose가 삽입되어 있는데, weight의 layout을 바꾸면 Transpose 자체를 생략할 수 있음

이런 경우 Transpose 연산을 제거하고 tensor를 다음 연산에 직접 연결하면 unnecessary memory copy를 줄일 수 있습니다.  
실제로 multi-head attention에서는 head 간 layout 전환에서 상당한 overhead가 발생하는데, 이걸 줄이는 데 효과적입니다.

3.2.2 Shape Inference

딥러닝 모델에서는 Expand, Unsqueeze, Reshape 같은 연산이 생각보다 자주 등장합니다.  
이 연산들이 runtime에 shape를 계산하게 되면, 작은 연산이라도 누적될수록 overhead가 생기게 됩니다.

Shape Inference는 static graph의 특성을 이용해서 모든 tensor의 shape를 export 시점에 확정하는 기법입니다.  
즉, 연산 시점에 shape를 계산하는 게 아니라, 그래프 상에서 미리 계산해두는 것이죠.

예를 들어, 특정 입력이 항상 (batch_size, 512)라고 가정할 수 있다면,  
이 input에 적용되는 모든 reshape, expand 연산의 결과도 미리 계산해서 고정된 연산자로 바꿔줄 수 있습니다.  
이렇게 하면 runtime에서 shape 연산 자체를 생략할 수 있고, memory도 사전에 미리 할당 가능해집니다.

3.2.3 Common Subexpression Elimination (CSE)

Common Subexpression Elimination, 줄여서 CSE는 말 그대로 중복되는 연산을 한 번만 수행하고 결과를 재사용하는 기법입니다.  
Transformer 모델처럼 구조가 반복되는 경우, 동일한 연산이 여러 번 수행되는 일이 자주 발생합니다.

대표적인 예시는 다음과 같습니다:

  • 각 attention layer에서 동일한 attention mask를 반복 생성  
  • 여러 block에서 동일한 positional embedding을 반복 계산  

이런 경우, mask나 embedding을 한 번만 계산하고 모든 layer에서 재사용할 수 있도록 그래프를 구성하면 됩니다.  
연산량 자체가 줄어들고, 메모리도 절약되며, 실행 trace도 더 단순해지는 장점이 있습니다.

단, 이 최적화는 연산이 pure function일 때만 안전하게 적용할 수 있습니다.  
즉, 같은 입력이 들어오면 항상 같은 출력이 나오고, side-effect가 없어야 합니다.

3.2.4 Dataflow-Level Optimization 정리표

Optimization Technique 주요 대상 패턴 적용 효과
Transpose Elimination Inverse transpose pair memory 이동 제거, 연산 수 감소
Shape Inference Expand, Reshape, Unsqueeze shape 계산 제거, memory 사전 계획 가능
Common Subexpr.
Elimination
attention mask, embedding
중복 제거
중복 연산 제거, tensor 재사용,
그래프 간결화

3.2.5 Dataflow-Level Optimization 예시

이번에는 실제로 자주 등장하는 데이터 흐름 최적화 예시들을 정리해보겠습니다.  
특히 Transformer 구조에서는 attention 블록, projection, reshape 연산 등이 반복되기 때문에,  
이런 부분에서 불필요한 연산을 제거하는 것만으로도 꽤 큰 성능 개선을 얻을 수 있습니다.

[예시 3.2.1] Transpose Pair 제거

Transformer의 Multi-head Attention에서는 head dimension을 맞추기 위해 Transpose 연산을 반복하는 경우가 많습니다.  
예를 들어 다음과 같은 연산 흐름이 있다고 해봅시다:

x → Linear → Transpose(0,2,1,3) → ScaledDotProductAttention → Transpose(0,2,1,3) → Output

여기서 두 개의 Transpose가 서로 역관계처럼 보이지만, 실제로는 같은 perm을 반복해서 호출하는 구조입니다.  
결과적으로는 원래와 동일한 레이아웃을 계속 유지하면서, 불필요하게 메모리 이동을 두 번이나 수행하게 되는 셈이죠.

이런 경우 두 Transpose 연산을 제거하고, attention 연산의 입력 layout을 직접 맞춰주는 방식으로 최적화할 수 있습니다.

최적화 전

x → Transpose(0,2,1,3) → Attention → Transpose(0,2,1,3)

최적화 후

x → Attention (입력 layout 직접 매핑)

이 최적화는 TensorRT나 ONNX optimizer의 eliminate_nop_transpose 같은 pass에서 자동으로 수행됩니다.

[예시 3.2.2] Transpose + MatMul Layout 통합

PyTorch의 Linear 연산은 내부적으로 다음처럼 구현됩니다:

y = x @ W.T + b

하지만 ONNX로 export할 경우 .T 연산이 명시적인 Transpose로 고정되며,  결국 불필요한 Transpose 연산자가 graph에 삽입됩니다. 예를 들어:

x: (B, S, D_in) → Transpose(x) → MatMul(W) → Output

이때 W는 학습 시 고정된 weight이므로, Transpose를 x에 적용하는 대신 W 자체를 미리 Transpose해둘 수 있습니다.  
즉, MatMul(x, W.T)를 그대로 표현하는 대신 MatMul(x, new_W)로 표현해 Transpose 연산을 생략하는 것입니다.

최적화 전

x → Transpose → MatMul(W)

최적화 후

x → MatMul(W.T)

이 방식은 memory 이동을 줄이고, weight layout을 고정함으로써 tensor 연속성을 확보할 수 있다는 장점이 있습니다.

[예시 3.2.3] Static Shape Inference

Transformer에서는 Expand, Unsqueeze, Reshape 같은 shape 관련 연산이 많이 사용됩니다.  
이들 연산은 PyTorch에서는 자동으로 처리되지만, ONNX나 TensorRT에서는 실행 시점에 shape를 계산해야 해서  
추론 시 오버헤드가 생기게 됩니다.

예를 들어 다음과 같은 흐름이 있다고 합시다:

x → Unsqueeze(dim=1) → Expand(…, H, …) → Reshape → Linear

이런 경우, export 전에 입력 shape를 고정하고, 모든 shape 관련 연산에 정수 constant를 사용하면  
runtime shape 계산을 완전히 없앨 수 있습니다.

최적화 전

x: dynamic shape → shape 계산 연산 → Reshape(x, shape)

최적화 후

x: static shape → Reshape(x, [4, 64, 128])

이 방식은 memory를 미리 할당할 수 있고, 추론 경로를 단순화할 수 있는 효과가 있습니다.

[예시 3.2.4] Attention Mask 중복 제거 (CSE)

Transformer에서는 각 layer가 동일한 attention mask를 사용하지만,  
그래프 export 시 각 layer가 동일한 연산을 반복 수행하는 구조로 만들어지는 경우가 많습니다.  
결과적으로 identical한 mask가 layer마다 중복 생성되어 비효율적인 실행이 발생합니다.

이 경우 mask를 한 번만 계산하고 모든 layer에서 공유하도록 graph를 재구성할 수 있습니다.

최적화 전

input_ids → mask_fn → attn_mask_1  
          → mask_fn → attn_mask_2  
          → mask_fn → attn_mask_3 ...

최적화 후

input_ids → mask_fn → shared_attn_mask  
                          ↓     ↓     ↓  
                         모든 attention block

이 방식은 연산량을 줄이는 동시에 그래프를 단순화하고 메모리 사용량도 절감할 수 있습니다.  
ONNX에서는 common_subexpression_elimination pass로 이런 최적화를 적용할 수 있습니다.

3.3 Memory & Resource Optimization

추론 환경에서는 모델 정확도만큼이나 중요한 것이 메모리 사용량과 실행 안정성입니다. 특히 Transformer 기반 모델은 시퀀스 길이나 히든 차원 크기 등으로 인해 중간 activation tensor가 매우 커질 수 있어, 메모리 최적화가 곧 성능이라고 해도 과언이 아닙니다.

이 섹션에서는 inference 효율을 높이기 위한 네 가지 메모리 최적화 기법을 다룹니다.  
핵심은 불필요한 버퍼를 줄이고, 가능한 자원을 공유하거나 낮은 정밀도를 사용하는 방식으로 효율성을 높이는 것입니다.

3.3.1 Memory Reuse

모델의 연산 흐름을 따라가다 보면, 중간 결과로 생성된 tensor가 더 이상 쓰이지 않는데도 계속 메모리에 남아있는 경우가 있습니다.  
Memory Reuse는 이러한 tensor들의 생존 주기(lifetime)를 분석해, 이후 연산에서 같은 메모리 공간을 재활용하는 방식입니다.

예를 들어 Transformer block에서 다음과 같은 연산이 있다고 해보죠:

Linear → GELU → Linear → Add → LayerNorm

GELU의 입력은 이후에 다시 사용되지 않기 때문에, 이 메모리 공간을 다음 Linear 연산에서 재활용할 수 있습니다.  
이런 방식은 static graph 상에서 live-range 분석을 통해 자동으로 최적화되며, peak memory 사용량을 상당히 줄일 수 있습니다.

3.3.2 Inplace Computation

보통 연산 결과는 새로운 메모리 버퍼에 저장되지만, 때로는 입력 tensor를 그대로 덮어쓰는 방식으로 memory 사용을 줄일 수 있습니다.  
이를 Inplace Computation이라고 하며, PyTorch에서는 inplace=True 옵션으로, ONNX나 TensorRT에서도 일부 연산에서 지원합니다.

예를 들어 LayerNorm 연산은 일반적으로 다음처럼 수행됩니다:

out = LayerNorm(x)

하지만 x가 이후에 사용되지 않는다면 아래와 같이도 쓸 수 있습니다:

LayerNorm(x, out=x)

이렇게 하면 불필요한 메모리 복사를 막을 수 있고, 전체 메모리 footprint도 줄어듭니다.

3.3.3 Weight Sharing / Buffer Folding

모델 구조를 보면 positional embedding이나 query/key projection weight처럼, 완전히 동일한 값이 여러 블록에 중복 포함되는 경우가 많습니다.  
Weight Sharing은 이런 constant 또는 weight tensor들을 복제하지 않고, 하나의 메모리 참조로 공유하는 방식입니다.

예를 들어 Q와 K의 projection weight가 동일할 경우, 별도로 저장하는 대신 하나의 buffer를 공유하게 만들 수 있습니다.  
이렇게 하면 전체 모델 크기를 줄일 수 있을 뿐만 아니라, GPU 메모리 초기화 속도도 개선됩니다.

Constant Folding vs Weight Sharing
이 두 기법은 자주 혼동되지만, 적용 대상과 방식이 다릅니다.  
Constant Folding은 연산 결과를 미리 계산해서 runtime 연산 자체를 없애는 방식이고,  
Weight Sharing은 동일한 tensor를 여러 번 복제하지 않고 하나로 참조하는 방식입니다.
항목 Weight Sharing / Buffer Folding Constant Folding
목적 동일 tensor 복제를 피하고 공유 정적인 연산 결과를 runtime 전에 계산
대상 positional embedding,
shared weight 등
add, mul, reshape 등 계산 가능한 연산
방식 tensor를 단일 buffer로 연결 연산 자체를 제거하고 상수로 치환
효과 model size 감소, memory 절감 latency 개선, 연산량 감소

3.3.4 Mixed Precision

추론에서는 꼭 모든 연산을 32비트 float(FP32)로 할 필요는 없습니다.  
많은 경우 16비트 부동소수점(FP16, BF16)으로도 충분히 안정적인 결과를 얻을 수 있으며, 이때 memory 사용량과 연산량이 함께 줄어드는 장점이 있습니다.

하지만 일부 연산은 여전히 높은 정밀도가 필요합니다. 대표적으로 LayerNorm, Softmax는 작은 값이나 큰 값을 다루기 때문에, FP16에서는 underflow나 overflow가 발생할 수 있습니다. 그래서 보통은 주요 연산만 FP32로 유지하고, 나머지는 FP16으로 전환하는 mixed precision 방식이 널리 쓰입니다.

AMP(Automatic Mixed Precision)나 loss scaling 같은 기법을 함께 사용하면 안정성을 확보할 수 있습니다.

3.3.5 Memory & Resource Optimization 정리표

Optimization Technique 대표 적용 예시 기대 효과
Memory Reuse 중간 tensor를 다음 연산에서 재활용 메모리 allocation 감소, peak 사용량 완화
Inplace Computation LayerNorm 등 결과를 입력 tensor에 overwrite memory 복사 제거, 전체 footprint 감소
Weight Sharing / Folding Q/K weight 공유, embedding 중복 제거 모델 크기 및 load 시간 절감
Mixed Precision 대부분 FP16, 주요 연산만 FP32 유지 memory 사용 절반, latency 개선

3.3.6 Memory & Resource Optimization 예시

[예시 3.3.1] FFN Block 내 Memory Reuse

Feed-forward block은 다음과 같은 구조로 이루어져 있습니다:

x → Linear1 → GELU → Linear2 → Add(residual) → LayerNorm

여기서 Linear1의 출력인 y1은 GELU를 거친 이후에는 더 이상 사용되지 않지만, 대부분의 그래프에서는 y1을 별도 tensor로 유지합니다. 이로 인해 불필요한 메모리 사용이 발생하고, 긴 입력 시퀀스에서는 OOM(Out-Of-Memory) 위험이 커집니다.

이 문제는 live-range 분석을 통해 해결할 수 있습니다. y1의 생존 범위를 파악한 후, 이후 연산에서 이 메모리 버퍼를 재활용하도록 설정하면 됩니다.  
TensorRT, ONNXRuntime 등의 프레임워크는 static memory planning을 통해 이 최적화를 수행합니다.

적용 효과  

  • 불필요한 메모리 allocation 제거  
  • peak memory usage 감소  
  • 긴 시퀀스나 대규모 batch 처리에서 안정성 향상

[예시 3.3.2] LayerNorm Inplace 처리

LayerNorm은 입력과 출력의 shape가 동일하기 때문에, 조건만 맞는다면 출력 값을 입력 tensor에 그대로 덮어쓸 수 있습니다.

# 일반적인 경우
y = LayerNorm(x)

# Inplace 적용
LayerNorm(x, out=x)

이렇게 처리하면 메모리 복사를 줄이고, 전체 메모리 사용량도 줄일 수 있습니다.  
PyTorch에서는 inplace=True 옵션을 통해 가능하며, TorchScript나 ONNX로 export할 때도 graph rewrite를 통해 적용할 수 있습니다.

적용 조건  

  • x가 이후에 사용되지 않아야 함  
  • LayerNorm이 deterministic하고 side-effect가 없어야 함

적용 효과  

  • output buffer 제거  
  • 메모리 footprint 감소  
  • 반복되는 layer 수가 많을수록 효과 누적

[예시 3.3.3] Positional Embedding Weight Sharing

Transformer 구조에서 sin/cos positional embedding 또는 learnable embedding은 여러 layer에서 동일하게 사용됩니다.  
하지만 ONNX로 export할 경우, 각 layer에 embedding이 복제되는 경우가 많아 불필요한 메모리 낭비가 발생합니다.

이를 해결하기 위해, embedding tensor를 하나의 shared buffer로 folding하고 그래프 상에서 모든 layer가 이를 참조하도록 변경할 수 있습니다.  
TensorRT, ONNXRuntime에서는 constant folding 및 buffer deduplication pass를 통해 이 최적화를 자동 수행할 수 있습니다.

적용 효과  

  • 모델 크기 감소  
  • constant tensor의 duplication 제거  
  • 초기화 및 로딩 속도 개선

[예시 3.3.4] Mixed Precision 적용 (FP16)

Transformer 모델을 FP32로 실행하면 연산 정확도는 유지되지만, 메모리 사용량과 연산 비용이 높아집니다.  
특히 large model, long sequence 환경에서는 GPU 리소스가 금세 한계에 도달합니다.

이 문제를 해결하기 위해 대부분의 연산을 FP16이나 BF16으로 변환하고, 민감한 연산(LayerNorm, Softmax 등)만 FP32로 유지하는 mixed precision 전략을 적용할 수 있습니다.  
PyTorch AMP, ONNX precision pass, TensorRT builder 등에서도 이러한 설정을 지원합니다.

적용 조건  

  • 연산 결과가 underflow/overflow에 민감하지 않을 것  
  • 수치 안정성을 보장할 수 있는 AMP, loss scaling 사용

적용 효과  

  • tensor 크기 절반 → GPU memory 사용량 절감  
  • Tensor Core 최적화 → 연산 속도 개선  
  • mixed precision 특화 GPU(A100, H100 등)에서 최대 효율 달성

3.4 Execution Graph Optimization

Execution graph 최적화는 모델 전체의 연산 흐름을 분석해, 불필요한 분기나 중복된 노드, 사용되지 않는 출력 등을 제거하고, 반복적인 subgraph 구조를 재구성하는 작업입니다.  
Transformer처럼 고정된 구조가 반복되는 모델에서는 특히 효과가 큰 영역으로, 연산 효율뿐 아니라 graph 분석 속도, 메모리 사용량 개선에도 직결됩니다.

대표적으로 자주 적용되는 최적화 기법은 아래와 같은 네 가지입니다.

3.4.1 Control Flow Simplification

모델 내부에는 if, Select, Where, Loop 같은 control flow 연산이 포함되어 있는 경우가 많습니다.  이 중 실행 시점에 항상 같은 조건을 따르는 분기가 있다면, 이를 제거하고 고정된 경로로 단순화할 수 있습니다.

예를 들어 decoder-only 모델에서는 cross-attention 여부를 if 조건문으로 다루는데, export 시점에 이미 decoder-only로 확정된 모델이라면 해당 분기 자체가 불필요합니다.

이런 control flow를 제거하면 그래프가 더 간결해지고, 실행 경로도 최적화됩니다.  
ONNX, TorchScript 등에서는 이를 위한 최적화 패스를 기본 제공하며, 분기 제거만으로도 cross-attention 유무에 따라 latency가 차이 나기도 합니다.

3.4.2 Redundant Node Pruning

의미 없는 노드를 제거하는 기법입니다.  
예를 들어 다음과 같은 연산 흐름은 실질적인 계산을 하지 않지만, tracing이나 export 과정에서 종종 생성됩니다:

x → Cast(float32 → float32) → Identity → x

이처럼 입력과 출력이 동일한 연산(Cast, Identity 등)은 제거하는 것이 바람직합니다.  
Dropout(p=0) 같은 연산도 inference 시점에서는 무의미하므로 pruning 대상이 됩니다.

이 작업은 노드 수 자체를 줄이고 graph size도 작게 만들어, compile 속도와 optimizer의 분석 시간을 단축시킬 수 있습니다.

3.4.3 Dead Output Elimination

Transformer 모델의 출력에는 보통 logits 외에도 hidden_states, attentions, past_key_values 등이 포함됩니다.  
하지만 실제 서비스에서는 logits만 사용하는 경우가 대부분입니다.

이때 사용되지 않는 출력 값을 제거하면, 해당 output을 생성하는 연산도 같이 줄일 수 있습니다.  
예를 들어 hidden_states를 생성하는 MLP나 projection 연산들이 제거 대상이 될 수 있죠.

output 수가 줄면 그래프가 간결해지고, memory 사용량과 latency가 줄어드는 이점도 따라옵니다.

3.4.4 Subgraph Folding & Pattern Matching

Transformer 구조에서는 attention block이나 FFN block처럼 반복되는 연산 조합이 많습니다.  이런 subgraph들을 인식해서 하나의 최적화된 커널로 묶는 방식이 subgraph folding입니다.

예를 들어 다음과 같은 attention subgraph:

QKV → MatMul → Scale → Softmax → MatMul → Add → Output

이 구조를 개별 연산으로 실행하면 memory 이동이 많고 launch overhead도 커집니다.  
TensorRT나 ONNXRuntime EP 등에서는 이 패턴을 자동 인식해서 FusedAttention 같은 커스텀 op로 변환해주기도 합니다.

적용 조건만 맞는다면, 연산 수가 5~10개에서 1개로 줄고, 메모리 접근도 대폭 감소합니다.

3.4.5 Execution Graph Optimization 정리표

최적화 기법 대표 패턴 기대 효과
Control Flow Simplification if, Select, Dropout(training) 실행 경로 단순화, 분기 제거
Redundant Node Pruning Identity, Cast(x→x), Dropout(p=0) 불필요 연산 제거, 노드 수 감소
Dead Output Elimination hidden_states, attentions 출력 계산 제거, memory 및 latency 절감
Subgraph Folding QKV → MatMul → Softmax → MatMul 반복 subgraph 병합, fused kernel로 최적화

3.4.6 Execution Graph Optimization 예시

[예시 3.4.1] Control Flow Simplification – decoder-only 조건 분기 제거

Transformer decoder 모델에서는 decoder_only 설정 여부에 따라 cross-attention을 실행할지 말지를 결정하는 조건 분기가 존재합니다.

if decoder_only:
    cross_attention = None
else:
    cross_attention = cross_attention_block(...)

이 구조는 ONNX로 export할 때 If 노드로 고정되는데, 실제로는 대부분의 경우 decoder_only=True로 고정되어 있어 분기 경로가 항상 하나로 수렴합니다.  
그럼에도 불구하고 ONNX 그래프에는 If, Constant, Identity, 사용되지 않는 branch 등이 모두 포함돼 있어, 그래프가 불필요하게 복잡해지고 추론 성능에도 영향을 줍니다.

해결 방법  

  • 조건이 상수로 고정되어 있다면, 해당 분기를 완전히 제거하고 단일 경로로 고정  
  • 사용되지 않는 branch는 DCE(dead code elimination) 또는 remove_unused_branch 패스로 제거  
  • ONNX optimizer에서 eliminate_deadend, eliminate_branch 등이 자동 적용됨

적용 효과  

  • 불필요한 control flow 관련 노드 제거  
  • 사용하지 않는 cross-attention 서브그래프 제거  
  • 그래프 간결화 → 컴파일 속도 향상 및 실행 안정성 확보

[예시 3.4.2] Redundant Node Pruning – Identity & Cast 제거

PyTorch 모델을 ONNX로 export하거나 TorchScript로 tracing할 때, 내부적인 tracing 목적으로 Identity, Cast 연산이 삽입되는 경우가 자주 발생합니다.

예를 들어:

x → Cast(float32 → float32) → Identity → y

이런 연산들은 실제로는 아무런 효과가 없지만, 그래프에는 별도 노드로 존재하게 되며, 실행 시 불필요한 memory copy나 kernel 호출을 유발합니다. 특히 .to(), .detach(), .clone() 같은 연산들이 원치 않게 이런 노드로 바뀌는 경우가 많습니다.

해결 방법  

  • ONNX: eliminate_identity, eliminate_nop_cast 최적화 패스 적용  
  • TorchScript / torch.fx 기반 그래프에서도 remove_nop_nodes, simplify_graph 등으로 제거 가능

적용 효과  

  • 노드 수 최대 15% 감소  
  • launch overhead 및 memory 이동 최소화  
  • 전체 그래프가 가벼워지고 export 시간도 단축됨

[예시 3.4.3] Dead Output Elimination – hidden_states 제거

HuggingFace 기반 Transformer 모델은 다음처럼 다양한 출력을 제공합니다:

return {
    "logits": logits,
    "hidden_states": all_hidden_states,
    "attentions": all_attentions
}

그러나 실제 서비스 환경에서는 대부분 logits만 사용되고, 나머지 출력은 전혀 쓰이지 않습니다.  
그럼에도 export 시점에서 출력 시그니처를 명시하지 않으면, 모든 출력 경로가 그대로 그래프에 포함됩니다.

특히 hidden_states는 모든 layer의 출력을 누적하므로, 연산량과 메모리 사용량이 크게 증가합니다.

해결 방법  

  • 출력 시그니처에서 hidden_states, attentions 등을 제거  
  • ONNX: remove_unused_outputs, eliminate_unused_initializer 패스 적용  
  • TorchScript: 출력 필터링 wrapper 작성 후 tracing 수행

적용 효과  

  • layer별 hidden state 저장 연산 제거  
  • memory usage 및 latency 절감  
  • export된 모델의 크기와 TensorRT engine 빌드 시간 단축

[예시 3.4.4] Subgraph Folding – Fused Multi-Head Attention

Transformer에서 Self-Attention 블록은 다음처럼 여러 연산으로 구성됩니다:

x → Linear_q, Linear_k, Linear_v
  → MatMul(q, k^T) → Scale → Softmax → Dropout
  → MatMul(..., v) → Output projection

이 전체 subgraph가 독립된 연산자로 구성돼 있을 경우, 메모리 이동이 반복되고 launch overhead가 누적되면서 성능 저하로 이어집니다.

해결 방법  

  • 위 구조를 FusedMultiHeadAttention 같은 하나의 연산으로 folding  
  • TensorRT: MHA plugin을 통해 subgraph 패턴을 자동 인식 후 커널 최적화  
  • ONNX: HuggingFace Optimum, GraphSurgeon 등을 활용한 folding  
  • TorchScript: 패턴 매칭 기반의 연산자 대체 적용 가능

적용 조건  

  • attention 구조가 고정된 순서 및 shape을 가질 것  
  • backend가 해당 fused subgraph 패턴을 지원할 것

적용 효과  

  • 연산자 수 8~10개 → 1개로 병합  
  • attention block의 latency 감소 및 GPU resource 효율 향상  
  • subgraph 최적화로 engine 크기 및 컴파일 시간도 단축됨

3.5 Precision Reduction

Precision Reduction은 연산자의 데이터 타입을 float32(FP32)에서 float16(FP16), bfloat16(BF16), int8 등으로 축소해  
메모리 사용량과 연산량을 동시에 줄이는 최적화 기법입니다.

Transformer 구조는 대부분의 연산이 low-precision 환경에서도 안정적으로 수행되기 때문에,  
실제 inference latency는 precision 및 memory bandwidth에 의해 좌우되는 경우가 많습니다.

정밀도 축소는 크게 세 가지 방식으로 나눌 수 있습니다:  
Mixed Precision,
Dynamic Quantization,
Static Quantization

3.5.1 Mixed Precision (FP16/BF16)

Mixed Precision은 모델의 주요 연산을 FP16 또는 BF16으로 변환해 실행하는 방식입니다.  
이렇게 하면 메모리 사용량이 절반으로 줄어들고, GPU의 Tensor Core 같은 low-precision 연산 유닛을 활용할 수 있어 throughput도 향상됩니다.

Transformer 모델은 대부분 연산에서 정밀도 손실에 강건하기 때문에, mixed precision 적용이 비교적 수월한 편입니다.

적용 방식  

  • 대부분의 연산은 FP16/BF16으로 수행  
  • 다만 Softmax, LayerNorm, exp, var 등은 수치 불안정성 문제로 FP32 유지  
  • PyTorch AMP, TensorRT, ONNX Runtime 등에서 자동 또는 수동 적용 가능

주의할 점  

  • Softmax나 LayerNorm은 underflow, overflow 이슈가 있으므로 FP32 fallback이 일반적  
  • AMP를 쓸 경우 내부적으로 자동 처리되지만, 수동 최적화 시 수치 안정성을 반드시 확인해야 합니다

3.5.2 Dynamic Quantization

Dynamic Quantization은 weight를 INT8로 변환하되, activation은 runtime에 실시간으로 quantize/dequantize하는 방식입니다.  
즉, 실행 시 입력 분포를 기반으로 scale과 zero-point를 동적으로 계산합니다.

연산 흐름 예시  

float_input → Quantize(scale, zero_point) → int8_input  
            → Dequantize(scale, zero_point) → float → 연산 수행

장점  

  • calibration dataset이 필요 없어서 export가 간편함  
  • Conservative quantization으로 정확도 손실이 적음

한계  

  • activation quantization이 runtime에 이뤄지므로 GPU에서는 효과 없음  
  • 대부분의 GPU inference backend는 dynamic path를 지원하지 않음  
    → CPU 기반의 ONNXRuntime, DNNL, QNNPack 등에서만 성능 개선이 있음

3.5.3 Static Quantization (INT8)

Static Quantization은 weight뿐 아니라 activation도 모두 정수(INT8)로 고정하는 방식입니다.  
이 경우 calibration dataset을 통해 연산별 입력/출력의 min/max 값을 미리 측정해야 하며,  
quantization parameter는 export 시점에 삽입됩니다.

적용 흐름  

  • calibration 실행 → tensor range 측정  
  • ONNX 또는 PyTorch 모델에 Quantize-Dequantize(QDQ) 연산 삽입  
  • 최적화된 INT8 kernel로 컴파일

장점  

  • GPU에서도 INT8 kernel 실행 가능  
  • TensorRT, ONNXRuntime EP, OpenVINO 등에서 full INT8 graph로 실행 가능  
  • Transformer block 전체에 적용 가능 (Linear, Attention, FFN 등)

주의 사항  

  • 정확도 유지를 위해 calibration 품질이 중요  
  • LayerNorm, Softmax 등 민감한 연산은 FP32 fallback 설정이 필요할 수 있음

3.5.4 Precision Reduction 정리표

Optimization Technique 적용 대상 전체 조건 주요 효과
Mixed Precision
(FP16/BF16)
대부분 연산자 (GPU) AMP 적용 가능, overflow 회피 필요 latency 개선, memory 절반 사용
Dynamic Quantization Linear, MatMul 등 (CPU) calibration 불필요, GPU 미지원 memory 절감, CPU latency 개선
Static Quantization
(INT8)
Linear, Attention 등 (GPU) calibration 필요, 정적 graph 필요 model size 감소, GPU inference 최적화 가능

Transformer 구조는 attention, feed-forward, projection 등 대부분의 연산이 정밀도 축소에 잘 적응합니다.  
따라서 상황에 따라 적절한 quantization 기법을 선택하면, 정확도를 유지하면서도 상당한 성능 개선을 기대할 수 있습니다.

3.5.5 Precision Reduction 예시

[예시 3.5.1] Mixed Precision 적용 (FP16)

문제

Transformer 모델을 FP32로 실행할 경우, 모든 텐서가 4바이트 단위로 처리되어  
GPU memory bandwidth에 큰 부담을 주게 됩니다. 또한, Tensor Core 등 low-precision 연산 장치를 활용하지 못해  
고비용 연산(MatMul, Attention, Linear 등)의 latency가 커지는 원인이 됩니다.

특히 long sequence 처리나 large batch 처리 시, memory 부족(OOM)이 발생하기 쉬운 구조입니다.

해결 방법

  • 주요 연산을 FP16 또는 BF16 타입으로 전환  
  • LayerNorm, Softmax 등 수치 안정성이 중요한 연산은 FP32 precision을 유지  
  • PyTorch에서는 autocast 기반의 AMP(Automatic Mixed Precision) 적용  
  • ONNX export 시에는 precision override 또는 수동으로 cast 연산을 삽입  
  • TensorRT에서는 enable_fp16 builder flag를 설정하면 자동으로 FP16 kernel이 적용됨

적용 조건

  • 모델이 underflow/overflow에 민감하지 않아야 함  
  • AMP-safe 연산자 구성이 확보되어 있어야 함  
  • LayerNorm, Softmax 등은 selective precision 설정 가능해야 함

적용 효과

  • 텐서 크기 절반으로 감소 → GPU memory usage 감소  
  • Tensor Core 활용 가능 → 연산 속도 개선  
  • latency 감소, batch size 확장, memory footprint 절감 등 다방면에서 효율성 향상

주의사항

  • LayerNorm은 매우 작은 값을 다루기 때문에 FP16에서는 rounding error, underflow가 발생할 수 있음  
  • Softmax는 exp 연산을 포함하고 있어 값의 표현 범위를 벗어나는 overflow 가능성이 존재  
  • 이러한 연산은 일반적으로 FP32로 고정하거나 AMP fallback을 설정하여 대응  
  • TensorRT, Megatron, DeepSpeed 등 일부 inference engine은 Softmax 및 Norm 계열 연산을 자동으로 FP32로 유지함

[예시 3.5.2] Dynamic Quantization 적용 (INT8)

문제

Transformer 모델에서 Linear, MatMul 연산이 전체 연산량과 memory usage의 상당 부분을 차지합니다.  
특히 CPU 기반의 inference 환경에서는 연산 성능 확보가 어렵고, memory 효율도 낮습니다.

해결 방법

  • weight는 export 시점에 INT8로 quantize  
  • activation은 runtime에 동적으로 quantize-dequantize 처리  
  • ONNX export 시 Linear, MatMul 등에 QDQ(Quantize-Dequantize) subgraph 삽입
float_input → Quantize(scale, zp) → int8_input  
           → Dequantize(scale, zp) → float_input → 연산

적용 조건

  • Linear, MatMul 중심의 구조일 것  
  • activation clipping에 따른 precision 손실이 크지 않을 것  
  • GPU가 아닌 CPU 기반 inference를 대상으로 할 것

적용 효과

  • weight 메모리 사용량 감소  
  • PyTorch 및 ONNXRuntime CPU Execution Provider 기준 latency 개선  
  • calibration 없이 간편하게 export 가능

한계

  • 연산 자체는 여전히 float domain에서 수행됨 (INT8 연산 아님)  
  • GPU backend에서는 dynamic quantization path를 지원하지 않음  
  • 대부분의 latency 개선 효과는 CPU 환경에서만 나타남

[예시 3.5.3] Static Quantization 적용 (INT8)

문제

Transformer의 attention, feed-forward block, embedding projection 등은 연산량이 많고,  
FP32 기반에서는 memory와 latency 모두 부담이 큽니다. 특히 GPU에서 INT8 연산을 적용하려면  
activation 범위를 미리 고정해야 하는데, dynamic quantization 방식으로는 이를 처리할 수 없습니다.

해결 방법

  • calibration dataset을 사용해 각 연산자의 input/output 텐서의 min/max 범위를 측정  
  • 측정된 범위를 기반으로 scale과 zero-point를 고정  
  • ONNX export 시 QuantizeLinear / DequantizeLinear 노드를 삽입하여 QDQ 패턴 구성  
  • TensorRT, OpenVINO, QNN EP 등 INT8 kernel을 지원하는 backend에서 실행
float_input → Quantize → int8_input  
           × int8_weight  
           → Dequantize → float_output

적용 조건

  • 정적 입력 크기(static shape)를 갖는 모델일 것  
  • calibration 데이터가 실제 입력 분포를 잘 반영해야 함  
  • QDQ 패턴을 해석 가능한 backend를 사용할 것

적용 효과

  • weight 및 activation 모두 8bit화 → 모델 크기 감소  
  • 연산량 및 memory bandwidth 대폭 절감  
  • TensorRT 등에서 encoder layer 기준 latency 개선 및 throughput 향상  
  • GPU memory footprint 감소 + INT8 경로 최적화

주의사항

  • activation clipping 범위가 정확하지 않으면 accuracy drop 발생 가능  
  • 일부 수치적으로 민감한 연산(e.g., attention logits, residual add)은 per-channel quantization 필요  
  • Softmax, MatMul 등의 연산은 hybrid precision(fused op + FP32 fallback)으로 구성하는 것도 고려

이러한 정밀도 축소 기법들은 각각 장단점이 있으며, 사용하는 하드웨어, 모델 구조, 정확도 요구 수준에 따라  
적절한 조합을 선택하는 것이 중요합니다. 특히 Transformer 계열 모델은 대부분 연산이 low-precision 환경에 잘 대응하므로,  
성능 병목이 발생하는 상황에서 precision reduction은 가장 효과적인 최적화 중 하나가 될 수 있습니다.

3.6 Transformer-Specific Optimization

Transformer 모델은 내부적으로 동일한 연산 구조가 반복되는 특징이 있습니다.  
이러한 구조적인 특성 덕분에, 일반적인 연산 최적화 외에도 Transformer에 특화된 subgraph-level 최적화 기법을 적용할 수 있습니다.

특히 Attention, Rotary Embedding, LayerNorm 등은 연산 흐름이 고정되어 있어  
컴파일 타임 혹은 그래프 최적화 시점에 보다 강력한 병합(fusion) 및 사전 계산(precomputation)이 가능합니다.

3.6.1 Fused Multi-Head Attention

기법 개요  
Transformer의 Multi-Head Attention은 Q, K, V 생성 → head 분리 → scaled dot-product → softmax → weighted sum → head 병합 등의 단계로 구성됩니다.  
기본 구현에서는 각 단계가 개별 연산으로 처리되지만, 이들을 하나의 fused kernel로 통합하면 실행 효율이 크게 향상됩니다.

TensorRT, FlashAttention, ONNX Runtime EP 등에서는  
이 subgraph를 통째로 감지해 FusedMultiHeadAttention 형태의 고성능 연산으로 대체할 수 있습니다.

적용 방식  

  • QKV projection을 하나의 weight로 합침  
  • Softmax, dropout, masking 등도 내부적으로 포함한 fused op 사용  
  • 대부분의 backend에서 자동 또는 수동으로 패턴 매칭을 통해 적용

적용 효과  

  • kernel 호출 횟수 감소 → launch overhead 감소  
  • 중간 tensor 제거 → memory usage 감소  
  • attention block 전체 latency가 크게 줄어듦 (특히 GPU에서 효과 큼)

3.6.2 Rotary Embedding Precomputation

기법 개요  
Rotary Positional Embedding(RoPE)은 각 토큰의 위치 정보를 sin/cos 함수로 encoding합니다.  
하지만 이 연산은 입력값과 무관하게 deterministic하게 반복되므로, runtime이 아닌 export 시점에 미리 계산할 수 있습니다.

적용 방식  

  • 각 길이에 대한 sin/cos 값을 미리 계산해 static tensor로 삽입  
  • runtime에는 단순 indexing 연산만 수행  
  • ONNX export 시 constant folding으로 자동 처리하거나, 수동 buffer 삽입 가능

적용 효과  

  • sin, cos 연산 제거 → latency 절감  
  • rotary embedding 처리 속도 안정화 → token 단위 생성 속도 향상

3.6.3 Key/Value Caching (Decoder-only 모델)

기법 개요  
GPT와 같은 decoder-only 모델에서는 토큰 생성 시 이전 시점의 Key, Value를 계속해서 누적합니다.  
매 시점마다 K/V를 다시 계산하는 대신, 이미 계산한 값을 cache에 저장하고 재활용하는 방식으로 inference latency를 획기적으로 줄일 수 있습니다.

적용 방식  

  • decoding loop에서 생성된 K/V를 별도 GPU buffer에 저장  
  • 이후 토큰에서는 query만 새롭게 계산  
  • TensorRT, FasterTransformer, HuggingFace 등에서 지원

적용 효과  

  • 중복 계산 제거로 per-token latency 절감  
  • 특히 긴 문장 생성 시 누적 latency를 선형 이하로 억제 가능  
  • long-context 모델 구성에서도 memory usage와 속도 안정성 확보

3.6.4 기타 구조 특화 최적화

Transformer 구조에서는 다음과 같은 패턴들도 자주 등장하며, 이 역시 별도 연산자 수준이 아니라 구조 최적화로 처리할 수 있습니다.

  • Flash Attention  
    메모리 효율을 높이기 위한 attention 재구성 방식. O(n²) 복잡도를 줄이면서 연산 정확도를 유지함
  • Fused LayerNorm  
    평균, 분산, 정규화 연산을 하나의 kernel로 통합하여 memory copy를 줄임
  • Residual Add Fusion  
    residual connection을 다음 연산(MatMul, LayerNorm 등)과 병합하여 memory 이동 최소화
  • Decoder Loop Unrolling  
    GPT-style 모델에서 autoregressive 구조를 unroll하여 token 단위 latency 개선

3.6.5 Transformer-Specific Optimization 정리표

Optimization Technique 적용 대상 주요 효과
Fused Multi-Head Attention Q/K/V, Softmax, MatMul kernel 수 감소, memory 이동 최소화
Rotary Embedding Precompute Rotary pos embedding sin/cos 제거, token 생성 속도 안정화
Key/Value Caching decoder attention 반복 연산 제거, long-context 대응
Flash Attention / Fused LN attention, LayerNorm 연산 병합, 메모리 사용량 및 연산량 절감

특히 Multi-Head Attention block 전체를 fused op 또는 plugin 연산자로 대체하는 방식은 실제 환경에서 가장 큰 latency 개선 효과를 가져올 수 있으며, 대부분의 최신 inference backend에서도 가장 우선적으로 지원하는 최적화 대상입니다.

3.6.6 Transformer-Specific Optimization 예시

[예시 3.6.1] Fused Multi-Head Attention

문제  
기존 Multi-Head Attention 구조는 Q/K/V projection부터 head 분리, dot-product, softmax, weighted sum, 그리고 최종 output projection까지  
6~9개의 연산으로 분리되어 각각 kernel을 호출합니다.  
이 과정에서 중간 tensor가 GPU memory에 반복 저장되며, kernel launch overhead도 누적됩니다.  
특히 head 수가 많거나 sequence length가 긴 경우 성능 저하가 두드러집니다.

해결  

  • Q/K/V projection을 하나의 weight로 합쳐 FusedLinear 연산으로 처리  
  • 이후 head split부터 weighted sum까지를 하나의 fused kernel로 통합  
  • TensorRT: MultiHeadAttentionPlugin, FlashAttention: CUDA kernel 기반 최적화,  
    ONNX Runtime: com.microsoft.FusedAttention 등에서 지원

적용 효과  

  • kernel 호출 수 대폭 감소  
  • memory 이동 제거  
  • attention block latency 대폭 감소, decoder generation 속도 개선

[예시 3.6.2] Rotary Embedding Precomputation

문제  
RoPE는 sin/cos 함수를 기반으로 위치 정보를 인코딩하지만, 대부분의 구현에서는 이 값을 runtime에 매번 계산합니다.  
특히 긴 sequence에서는 token 수만큼 반복적인 sin/cos 연산이 발생해 latency가 누적되고,  
ONNX export 시에도 해당 연산이 그래프에 고정되어 최적화가 어려워집니다.

해결  

  • sin/cos 테이블을 export 시점에 미리 계산하여 constant tensor로 삽입  
  • runtime에서는 position index 기반 lookup만 수행  
  • PyTorch: register_buffer, ONNX: ConstantInitializer 방식으로 구현

적용 효과  

  • trigonometric 연산 제거 → rotary embedding latency 절감  
  • long-context 모델에서도 연산 안정화  
  • static graph export 시 constant folding 최적화 적용 가능

[예시 3.6.3] Key/Value Caching (Decoder-only Transformer)

문제  
decoder-only 구조에서는 autoregressive decoding 시 매 step마다 전체 context의 K/V를 다시 계산합니다.  
sequence 길이가 늘어날수록 연산량이 선형 증가하고, inference latency가 크게 증가합니다.

해결  

  • 이전 시점의 K/V를 kv_cache로 저장하고, query만 새로 계산  
  • 각 토큰에 대해 q_t × [K_1~K_t]로 attention 수행  
  • PyTorch: past_key_values, TensorRT: set_kv_cache(), ONNX: external buffer 연동 방식 활용

적용 효과  

  • K/V 중복 계산 제거  
  • token 단위 latency 대폭 감소  
  • 긴 시퀀스에서 linear scaling을 비선형 수준으로 억제

[예시 3.6.4] Flash Attention 및 기타 구조 특화 최적화

문제  
기존 attention은 q × k^T, softmax, v와의 곱을 각각 독립적으로 처리하며,  
중간 결과를 full precision으로 저장해 memory usage가 급격히 증가합니다.  
특히 long sequence에서 softmax overflow나 underflow 발생 가능성도 있습니다.

해결  

  • FlashAttention은 위 과정을 하나의 CUDA kernel로 통합  
  • Softmax를 streaming 방식으로 계산하여 intermediate tensor를 제거  
  • 전체 attention matrix를 메모리에 올리지 않고 block-wise로 처리

적용 효과  

  • memory usage 절감  
  • latency 대폭 감소  
  • backward에서도 메모리 절약 가능

기타 구조 특화 최적화  

  • Fused LayerNorm  
    • mean, variance, normalization을 단일 kernel로 처리  
    • memory copy 제거 및 latency 절감
  • Residual Add Fusion  
    • residual connection을 다른 연산(LayerNorm 등)과 병합  
    • kernel 호출 수 감소 + memory access 최적화
  • Decoder Loop Unrolling  
    • decoder loop를 고정된 반복 횟수로 펼쳐 static graph로 export  
    • caching 및 subgraph 최적화에 유리

이처럼 Transformer 구조에서 반복적으로 등장하는 연산 흐름에 대해 구조 수준의 병합 또는 사전 계산을 적용하면, 단순한 연산자 최적화보다 훨씬 큰 latency 개선 효과를 얻을 수 있습니다. 특히 real-time generation이나 long-context decoding이 필요한 환경에서는 필수적인 최적화 기법이라 할 수 있습니다.

3.7 Runtime-Aware Optimization

지금까지 살펴본 대부분의 최적화는 모델을 export하거나 컴파일하는 시점에 정적으로 적용되는 방식입니다. 하지만 실제 inference 환경에서는 입력 길이, batch size, GPU 종류, 메모리 상황 등 다양한 변수에 따라 최적의 실행 경로가 달라질 수 있기 때문에, runtime 시점에 적용되는 동적 최적화도 매우 중요합니다.

이 섹션에서는 런타임 상황을 고려한 다섯 가지 주요 최적화 기법을 다룹니다.

3.7.1 Kernel Auto-Tuning

개요  
동일한 연산이라도 kernel 설정(예: block size, tiling 전략, unroll factor 등)에 따라 성능 차이가 크게 납니다. Kernel auto-tuning은 다양한 설정을 미리 실행해보고 가장 빠른 구성을 선택해 실행하는 방식입니다.

구현 방식  

  • TensorRT: builder 단계에서 여러 kernel profile을 벤치마크하고 best kernel 선택  
  • PyTorch Inductor: Triton 기반 auto-launch config search

적용 조건  

  • tuning 시간이 허용 가능할 것  
  • 입력 shape과 device 정보가 안정적으로 유지될 것

효과  

  • operator-level latency 개선  
  • tuning 결과를 cache해 다음 실행부터 바로 활용 가능

3.7.2 Memory Layout Selection

개요  
tensor shape이 같더라도 memory layout(NCHW vs. NHWC, row/column major 등)에 따라  
cache hit rate, memory access 효율이 달라집니다.  
runtime에 최적의 layout을 선택하거나 변환해 연산 성능을 높일 수 있습니다.

구현 방식  

  • TensorRT: relayout 없이 실행 가능한 포맷 우선 적용 (예: kLINEAR, kCHW32)  
  • ONNX Runtime: preferred_layout 옵션을 통해 backend가 최적 layout 선택

적용 조건  

  • backend가 다양한 layout 경로를 지원할 것  
  • relayout 비용보다 얻는 성능 이득이 클 것

효과  

  • memory access locality 개선  
  • 일부 kernel에서 layout 최적화만으로도 큰 성능 향상 가능

3.7.3 Dynamic Shape Specialization

개요  
dynamic shape을 지원하는 모델에서는 자주 등장하는 입력 shape에 대해  
별도의 실행 계획을 생성해 캐시하고 재사용할 수 있습니다.  
동적으로 들어오는 입력에 대해서도 static graph 수준의 성능을 확보할 수 있는 전략입니다.

구현 방식  

  • TensorRT: optimization profile(min/opt/max shape) 사전 정의  
  • ONNX Runtime: runtime에 shape-specialized plan을 내부 캐시에 저장  
  • PyTorch Inductor: shape-specific IR을 trace 후 dispatch

적용 조건  

  • 입력 shape가 제한된 범위 내에서 반복될 것  
  • backend가 shape-aware plan caching을 지원할 것

효과  

  • dynamic shape에서도 실행 안정성 확보  
  • shape에 맞는 plan을 재사용해 latency를 줄일 수 있음

3.7.4 Execution Plan Caching

개요  
연산자의 scheduling, memory allocation, kernel launch 순서 등을 runtime 최초 실행 시 미리 생성하고  
동일 조건에서는 해당 계획을 재사용하는 방식입니다.

구현 방식  

  • TensorRT: serialized engine plan 저장  
  • ONNX Runtime: session 단위의 execution plan 캐시  
  • PyTorch 2.x: ahead-of-time으로 compile한 plan을 캐시

적용 조건  

  • 입력 조건이 고정되거나 유사한 상황이 반복될 것  
  • 캐시 관리 로직이 backend에서 자동 수행될 것

효과  

  • 최초 실행 이후 scheduling 비용 제거  
  • 실시간 추론에서 startup latency를 크게 줄일 수 있음

3.7.5 Fallback / Precision Promotion

개요  
FP16, INT8 같은 low-precision 연산에서 overflow, NaN 등의 문제가 발생할 경우,  
자동으로 FP32 연산으로 전환해 안정성을 확보하는 방식입니다.

구현 방식  

  • PyTorch AMP: autocast에서 자동 precision fallback 적용  
  • TensorRT: FP16 kernel 실패 시 FP32 경로로 자동 전환  
  • ONNX Runtime: quantization 실패 시 float fallback 처리

적용 조건  

  • precision fallback의 latency overhead가 수용 가능할 것  
  • precision-sensitive 연산자가 명확히 정의되어 있을 것

효과  

  • low-precision 연산 경로에서의 안정성 확보  
  • mixed precision 환경에서도 자동으로 품질 보장 가능

3.7.6 Runtime-Aware Optimization 정리표

Optimization Technique 적용 시점 주요 대상 기대 효과
Kernel Auto-Tuning compile/runtime MatMul, Conv 최적 kernel 선택, latency 개선
Memory Layout Selection runtime 모든 tensor 연산 memory locality 향상
Dynamic Shape Specialization runtime dynamic input 모델 shape별 plan 재사용, latency 안정화
Execution Plan Caching runtime 전체 graph 실행 scheduling 제거, startup latency 개선
Fallback / Precision Promote runtime FP16, INT8 경로 안정성 확보, 자동 precision 전환

runtime-aware 최적화는 개발자의 명시적 설정 없이도, 환경과 입력 조건에 따라 모델이 자동으로 더 빠르고 안정적으로 동작하도록 만드는 기반입니다. 특히 production 환경이나 API inference에서 latency와 안정성이 중요한 경우 이러한 최적화는 성능 튜닝의 핵심 도구가 될 수 있습니다.

3.7.7 Runtime-Aware Optimization 예시

[예시 3.7.1] Kernel Auto-Tuning (TensorRT builder)

문제  
같은 연산(MatMul, LayerNorm 등)이라도 block size, tiling 방식, memory layout 등에 따라 성능이 크게 달라질 수 있습니다.  
예를 들어 (32, 1024) × (1024, 4096) 크기의 MatMul 연산에서는 block 크기 설정에 따라 최대 2배 이상 성능 차이가 발생합니다.

해결  
TensorRT는 builder 단계에서 다양한 kernel configuration을 생성하고, micro-benchmark를 통해 가장 빠른 설정을 선택합니다.  
선택된 kernel 경로는 serialized engine에 포함되어 이후 실행 시 재활용됩니다.

구현 방식  

  • TensorRT: setMaxWorkspaceSize, setFlag(kENABLE_TACTIC_HEURISTIC) 등 설정  
  • PyTorch Inductor, TVM, XLA 등도 유사한 auto-scheduling 또는 autotuning 지원

적용 효과  

  • 연산자별 latency 최적화  
  • tuning은 최초 10~60초 소요되며 이후 캐시 재사용 가능  
  • GPU 아키텍처에 따라 최적 경로가 달라지는 상황에 특히 유용

[예시 3.7.2] Memory Layout Selection (ONNXRuntime + TensorRT)

문제  
같은 tensor shape이더라도 memory layout(NCHW vs. NHWC, row-major vs. column-major)에 따라 성능이 달라집니다.  
layout이 연산에 맞지 않으면 transpose가 삽입되어 memory copy와 latency overhead가 발생합니다.

해결  

  • ONNXRuntime은 execution provider의 preferred layout에 따라 연산자를 선택  
  • TensorRT는 내부적으로 최적의 layout(kLINEAR, kCHW32 등)을 자동 선택  
  • MLIR 기반 백엔드는 layout-aware scheduling을 수행

구현 방식  

  • ONNXRuntime: session option에서 layout-aware execution 설정  
  • TensorRT: kPREFER_PRECISION_CONSISTENT_LAYOUT 설정  
  • 일부 framework에서는 layout annotation → relay pass에서 전달

적용 효과  

  • transpose 제거로 memory 이동 최소화  
  • vectorization-friendly layout 사용  
  • Conv 연산 기준 latency 최대 40% 개선 사례 존재

주의사항  

  • framework마다 기본 layout 상이 (예: PyTorch=NCHW, TensorFlow=NHWC)  
  • layout 강제 변경 시 kernel fallback 가능성 존재

[예시 3.7.3] Dynamic Shape Specialization (TensorRT Optimization Profile)

문제  
Transformer 모델은 sequence length나 batch size가 유동적인 경우가 많습니다.  
이를 dynamic shape으로 처리하면 shape 해석과 memory planning 때문에 runtime overhead가 발생합니다.

해결  
자주 등장하는 shape에 대해 미리 여러 개의 optimization profile을 정의하고,  
runtime에 해당 shape에 맞는 profile을 선택해 실행합니다.

구현 방식  

  • TensorRT: create_optimization_profile() → min/opt/max shape 등록  
  • ONNXRuntime: ORT_ENABLE_PREPACK 설정으로 shape-specific plan 캐시  
  • PyTorch 2.x: TorchDynamo + Inductor 기반 shape-specialized graph 저장

적용 효과  

  • dynamic shape에서도 static graph 수준의 성능 확보  
  • shape별 latency 편차 감소 (40~60% → 5~10%)  
  • 다양한 입력 길이에 안정적으로 대응 가능

주의사항  

  • profile 수가 많아지면 engine size 증가  
  • unknown shape은 fallback profile로 실행되어 성능 저하 가능  
  • workload 기반의 shape 분석이 중요

[예시 3.7.4] Execution Plan Caching (ONNXRuntime SessionGraph)

문제  
ONNX 모델 최초 실행 시 graph 파싱, 연산자 스케줄링, memory plan 수립, kernel 할당 등의 초기화가 필요합니다.  
이 과정은 shape와 device config에 따라 달라지며 cold start latency가 수백 ms까지 증가할 수 있습니다.

해결  
한 번 생성된 execution plan을 cache로 저장하고, 동일한 조건에서 그대로 재사용합니다.

구현 방식  

  • ONNXRuntime: session 단위 plan cache (enable_mem_pattern, enable_cpu_mem_arena)  
  • TensorRT: serialized engine 자체가 plan  
  • PyTorch 2.x: AOT compile된 plan을 shape별로 저장해 dispatch

적용 효과  

  • startup latency 감소 (예: 220ms → 40~60ms)  
  • cold start 이슈 완화  
  • 서버 환경이나 multi-session inference에서 매우 유용

주의사항  

  • shape, device config가 다르면 cache 미적용  
  • dynamic shape에서는 shape-specific plan을 다수 보관해야 함

[예시 3.7.5] Fallback / Precision Promotion (Runtime Stability)

문제  
INT8 또는 FP16 환경에서는 일부 연산(LayerNorm, Softmax 등)에서 underflow, overflow, NaN이 발생할 수 있습니다.  
특히 Transformer 계열 모델은 정밀도 손실에 민감한 연산이 많습니다.

해결  
runtime 시점에서 위험한 연산만 자동으로 FP32로 승격하거나, 해당 연산을 다른 backend로 fallback 처리합니다.

구현 방식  

  • TensorRT: kSTRICT_TYPES 해제 시 fallback 허용  
  • ONNXRuntime: 지원하지 않는 연산 → CPU fallback  
  • PyTorch AMP: @custom_fp32_op decorator로 selective promotion 지정 가능

적용 효과  

  • NaN, INF 방지  
  • mixed precision 환경에서도 안정적인 output 확보  
  • 자동 precision 제어로 개발자 수작업 개입 최소화

주의사항  

  • fallback이 잦으면 low-precision 이득 상쇄  
  • device 간 tensor 이동이 발생할 수 있음  
  • profiling을 통해 fallback 발생 위치 추적 필요

이번 글에서는 실시간 RAG 시스템에서 발생하는 추론 병목 현상을 해결하기 위한 핵심 전략으로 ‘Computation Graph Optimization’의 필요성과 주요 기법들을 살펴봤습니다.

Operator Fusion, Constant Folding, Dead Code Elimination 등 operator-level 최적화부터, Dataflow-level과 Memory 최적화까지 각 단계별 접근법을 설명하며, Transformer 모델 특화 구조에 대한 인사이트도 함께 제공했습니다.

2부에서는 TorchScript, ONNX, TensorRT 등 대표적인 최적화 프레임워크를 비교 분석하고, 대규모 실험 결과를 기반으로 latency를 획기적으로 줄인 실제 사례를 공유합니다.

(2부 바로각: Computation Graph Optimization 2부 - 모델 추론 속도를 위한 그래프 구조 정리법

우리 회사에 최고 성능의 LLM을 도입하고 싶다면 '올거나이즈'에 문의하세요!