tech · 2026-03-22

Computation Graph Optimization

#rag#attention

1. 서론

1.1. 배경

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

  • Transformer 기반 모델은 고정밀 matrix 연산과 반복적인 self-attention 구조로 인해 추론 latency가 큼.
  • Retrieval-augmented generation (RAG) 시스템에서는 실시간 응답 요구가 높으며, 추론 지연이 전체 시스템 응답 품질에 영향 미침.
  • 대부분의 모델은 PyTorch 기반으로 구현되며, dynamic computation graph를 사용함. 이는 개발 유연성에는 유리하지만, 추론 최적화에는 불리하게 작용함.

1.2. 필요성

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

  • Retrieval 파이프라인의 주요 병목은 query encoding(bi-encoder)과 document re-ranking(cross-encoder) 단계에서 발생함.
  • Cross-encoder는 query와 document를 함께 인코딩하므로 계산 비용이 큼. 후보 문서 수에 따라 inference latency가 선형적으로 증가함.
  • 실시간 처리를 위해서는 per-query latency를 줄이는 최적화가 필요함. Static graph 변환 및 compiler-level 최적화가 핵심임.

1.3. 최적화 목표

  • Latency: 실시간 응답 요구를 만족하기 위해 inference time을 수십 ms 이내로 단축함.
  • Throughput: 동일 자원에서 처리 가능한 queries per second (QPS)를 극대화함.
  • Deployment: Python runtime 종속성을 제거하고 multithreaded 실행이 가능한 구조로 전환함.
  • 주요 최적화 항목은 operator fusion, constant folding, precision reduction (FP16/INT8), memory planning 등임.
  • 활용 프레임워크는 TorchScript(JIT compilation), ONNX(intermediate graph export and optimization), TensorRT(high-performance inference engine on NVIDIA GPUs) 등임.

2. 배경

2.1. Computation Graph 개념

  • 딥러닝 모델은 연산과 데이터 흐름을 Directed Acyclic Graph(DAG) 형태로 표현 가능함.
  • 노드는 연산자(operator), 엣지는 텐서(tensor)의 흐름을 나타냄.
  • 학습(training)에서는 autograd 엔진이 동적 그래프를 사용하여 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 등이며, 대부분 GPU 메모리 대역폭과 연산량을 많이 사용함.
  • 각 층의 attention 연산은 O(n²) 시간복잡도를 가짐. n은 시퀀스 길이(sequence length).
  • encoder-only 모델(BERT)은 전체 문장을 동시에 인코딩하며, decoder-only 모델(GPT 등)은 auto-regressive 구조로 반복적인 추론 경로를 가짐.
  • 특히 inference 시, 각 토큰에 대해 반복되는 연산 구조는 latency에 큰 영향을 줌.

2.3. Static Graph 변환의 이점

  • 연산 그래프를 static 형태로 고정하면, 전체 연산 흐름을 사전에 분석할 수 있어 다양한 최적화가 가능함.
  • constant folding, operator fusion, dead code elimination, memory reuse 등은 static graph 상에서 적용 가능함.
  • 반복되는 구조를 unroll하거나, 입력 shape이 고정된 경우 shape inference를 통해 memory allocation을 최적화할 수 있음.
  • dynamic graph는 유연성은 높지만 최적화 여지가 낮고, Python 인터프리터 오버헤드 및 GIL(Global Interpreter Lock) 이슈가 발생함.
  • static graph 기반으로 export하면 Python-free 추론이 가능하며, multithreaded 실행이나 low-level inference engine(TensorRT 등)에 최적화된 구조 제공 가능함.

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

  • Retrieval 시스템에서의 추론은 다음과 같은 두 시점에 발생함:

    • Async document encoding: 미리 임베딩을 생성해 저장 (bi-encoder)
    • Runtime Query Processing & Reranking: 사용자의 입력에 따라 실시간 추론 수행 (bi-encoder, cross-encoder)
  • 특히 cross-encoder는 질의와 각 문서를 결합해 개별 추론을 수행하므로, 문서 수가 많을수록 지연이 누적됨.

  • 실시간 검색에서는 300~500ms 안팎 수준의 지연이 요구되며, 미세한 최적화도 운영 품질에 직접적 영향을 미침.


3. Computation Graph Optimization Techniques

  • Computation graph는 모델 추론 시 수행되는 연산들의 정적 실행 흐름을 정의하며, latency 및 throughput 개선을 위한 최적화의 핵심 대상임.

  • Static graph 기반 최적화를 적용하면 연산 구조 간소화, 메모리 접근 감소, 실행 순서 개선, 연산 precision 변경 등이 가능해짐.

  • Transformer 모델은 attention, residual, projection block 등에서 반복적인 연산 패턴을 가지며, subgraph 단위 최적화가 latency 개선에 효과적임.

  • Computation graph 최적화는 일반적으로 다음 다섯 범주로 분류됨:

    • 연산 구조 최적화 (operator fusion, constant folding, dead code elimination 등)
    • 데이터 흐름 최적화 (transpose 제거, shape inference, CSE 등)
    • 메모리 및 자원 최적화 (memory planning, in-place operation 등)
    • 정밀도 축소 (FP16, INT8 quantization 등)
    • Transformer 특화 최적화 (fused attention, rotary embedding 정적 처리 등)
  • 최적화 기법은 정확도 손실 없이 latency를 줄이는 conservative 방식부터, precision trade-off를 허용하여 extreme throughput을 달성하는 aggressive 방식까지 다양함.

  • 대부분의 기법은 static graph 상에서 적용되며, ONNX, TorchScript, TensorRT 등의 backend에서도 유사하게 활용됨.


3.1 Operator-Level Optimization

  • 연산 구조 최적화는 연산자(operator) 수준의 병합 및 단순화를 통해 kernel 호출 횟수, memory access 수, 연산량을 줄이는 것을 목표로 함.
  • 주로 static computation graph 상에서 정적 분석을 통해 수행되며, Transformer 구조에서는 반복되는 subgraph 단위 최적화로 latency 개선 효과가 큼.
  • 세가지 주요 연산 구조 최적화 기법: operator fusion, constant folding, dead code elimination.

3.1.1 Operator Fusion

  • 연속된 연산을 하나의 fused kernel로 결합하여 연산 간 메모리 이동을 제거하고 실행 효율을 높임.
  • 주로 Linear, Add, LayerNorm, GELU, Residual 등 인접 연산자들이 대상이 됨.

기법 개요

  • 연산자 간 intermediate tensor 생성을 제거하여 memory bandwidth 사용을 최소화함
  • kernel 호출 수 감소를 통해 GPU context switching 및 launch overhead를 줄임

대표 패턴

  • 문제: Linear → Add → LayerNorm 구조가 개별 연산자로 분리되어 있어 kernel 호출과 memory access가 많음
  • 해결: FusedLinearNorm으로 병합하여 단일 kernel로 실행 → [예시 3.1.1]
  • 문제: Q/K/V projection이 각각 수행되고 이후 Concat 및 Transpose가 따로 실행됨
  • 해결: FusedQKVProjection으로 하나의 연산자로 결합 → [예시 3.1.2]

적용 조건

  • 연산자 간 데이터 의존성이 선형 구조를 유지할 것
  • tensor shape이 정적이고 broadcasting 규칙이 명확히 정의되어 있을 것
  • control flowbranching이 없을 것

적용 효과

  • kernel 수 감소, 메모리 이동 감소 → latency 단축
  • 실측 기준 TensorRT에서 BERT 모델 latency 20~30% 감소 사례 존재
  • 대규모 모델에서 layer 반복 시 효과 누적됨

3.1.2 Constant Folding

  • 상수 연산을 그래프 변환 또는 export 시점에 미리 계산하여 runtime 계산을 제거함

기법 개요

  • 상수끼리의 연산 결과를 정적으로 계산하고, 연산자 대신 고정된 tensor로 대체함

대표 패턴

  • 문제: Rotary embedding에서 runtime에 sin/cos 계산이 반복 수행됨
  • 해결: PrecomputedRotaryEmbedding으로 export 시점에 사전 계산 → [예시 3.1.3]
  • 문제: Attention mask 및 layer normalization 초기 파라미터 계산이 매번 수행됨
  • 해결: constant로 변환하여 static tensor로 삽입

적용 조건

  • 연산의 입력이 모두 constant일 것
  • shape 및 dtype이 export 시점에 명확히 고정되어 있어야 함

적용 효과

  • runtime 연산 제거로 latency 안정화
  • 초기화 속도 개선 및 그래프 간결화
  • ONNX, TensorRT, TVM 등에서 기본적으로 지원됨

3.1.3 Dead Code Elimination (DCE)

  • 추론 시 사용되지 않는 연산자 및 노드를 제거하여 그래프를 간소화하고 리소스 낭비를 방지함

기법 개요

  • 학습 전용 연산이나, 경로에서 사용되지 않는 노드를 정적으로 분석해 제거함

model.eval()과의 관계 및 차이점

  • model.eval()은 Dropout, BatchNorm 등의 동작 방식만 바꾸며, 연산 자체는 그래프에 남아 있음
  • DCE는 이러한 연산자 자체를 그래프에서 완전히 제거하는 최적화 작업임
  • Dropout이 단순히 '비활성화'되는 것이 아니라, DCE를 통해 아예 실행 경로에서 제거되면
    메모리 할당, 연산 오버헤드 자체가 사라져 실질적인 latency 및 메모리 이득이 발생함
  • 즉, model.eval()은 전제 조건이고, DCE는 연산 이득이 발생하는 실제 최적화 단계임

대표 패턴

  • 문제: Dropout 연산이 inference 시점에도 그래프에 포함되어 있음
  • 해결: DropoutRemoval을 통해 그래프 export 시 제거 → [예시 3.1.4]
  • 문제: Loss, backward 연산 등이 제거되지 않아 memory overhead 유발
  • 해결: DCE pass로 dead node 제거

적용 조건

  • PyTorch 기준 model.eval() 상태에서 export가 이루어져야 함
  • 그래프 변환기나 optimizer가 DCE pass를 내장하고 있어야 함

3.1.4 Operator Fusion 정리표

최적화 기법 주요 대상 패턴 적용 효과
Operator Fusion Linear + Add + LayerNorm, Q/K/V + Transpose kernel 수 감소, memory access 최소화
Constant Folding sin/cos 계산, static mask, 초기값 계산 runtime 계산 제거, 초기화 시간 단축
Dead Code Elimination Dropout, Loss, Gradient 연산 불필요 노드 제거, memory 사용 절감

3.1.5 Operator Fusion 예시


[예시 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)을 한 block에서 실행  
- TorchScript, ONNX Runtime, TensorRT 등에서 연속 패턴 감지 후 자동 적용

적용 효과  
- kernel 호출 수: 4 → 1  
- 중간 tensor 저장: 3개 → 0개  
- latency 18~25% 감소 (BERT-base, A100 기준)
[예시 3.1.2] FusedQKVProjection

문제  
Self-attention 입력을 생성할 때 다음과 같은 연산이 수행됨:  
x → Linear(W_q), Linear(W_k), Linear(W_v) → concat → transpose

총 3개의 Linear와 2개의 tensor 연산으로 인해 memory copy, kernel call 증가

해결  
- W_q, W_k, W_v를 하나의 W_qkv로 합쳐 Linear 1회로 처리  
- 이후 concat + transpose 포함한 fused projection kernel로 최적화  
- TensorRT MHA Plugin, Optimum ONNX 패턴 등에서 사용됨

적용 효과  
- Linear 연산 수: 3 → 1  
- memory 이동 제거  
- attention 준비 latency 최대 30% 감소
[예시 3.1.3] PrecomputedRotaryEmbedding

문제  
Rotary embedding 계산 시 매 토큰마다 다음 연산 반복됨:  
θ × pos → sin, cos → embedding 곱셈

특히 긴 시퀀스에서 trigonometric 연산 반복으로 latency 증가

해결  
- export 시 sin/cos 값을 미리 계산하여 고정 tensor로 삽입  
- runtime에서는 indexing만 수행  
- constant folding에 의해 embedding 연산이 완전히 정적 처리됨

적용 효과  
- trigonometric 연산 제거  
- rotary embedding 블록 latency 70% 감소  
- decoder token 생성 속도 안정화
[예시 3.1.4] DropoutRemoval (Dead Code Elimination)

문제  
Dropout(p=0.1)이 inference graph에도 남아 있어 실행 시 memory buffer가 생성됨  
예: x → Linear → Dropout → Add → LayerNorm

해결  
- ONNX export 후 DCE 패스를 통해 Dropout 연산자 제거  
- 입력과 출력을 직접 연결하여 runtime 연산 자체 제거

적용 효과  
- Dropout 관련 메모리 완전 제거  
- 실행 경로 간소화  
- TensorRT 최적화 단계 그래프 처리 시간 약 15% 단축

3.2 Dataflow-Level Optimization

  • Dataflow-level optimization은 연산 간 tensor 이동 경로를 최소화하고 중복 연산을 제거하여, memory access와 runtime overhead를 줄이는 것을 목표로 함.
  • Transformer 구조는 attention, residual, projection block 등에서 shape 변경, transpose, broadcast 연산이 반복되므로, 이러한 최적화가 latency와 memory 사용량에 직접적인 영향을 줌.
  • 세가지 주요 dataflow-level 기법: transpose elimination, shape inference, common subexpression elimination.

3.2.1 Transpose Elimination

기법 개요

  • 연속된 transpose 연산 또는 불필요한 layout 전환 연산을 제거하여 memory 이동을 줄임
  • Transpose는 연산량이 적지만 memory bandwidth를 많이 소모하며, GPU cache 활용을 방해함

대표 패턴

  • 문제: x → Transpose(A→B) → Transpose(B→A) → 다음 연산
  • 해결: Transpose pair를 제거하고 x를 다음 연산에 직접 연결 → [예시 3.2.1]
  • 문제: MatMul 앞뒤로 불필요한 Transpose가 삽입됨
  • 해결: MatMul 자체의 weight layout을 변경하여 Transpose 생략 → [예시 3.2.2]

적용 조건

  • 상쇄 가능한 inverse Transpose가 연속적으로 등장할 것
  • 후속 연산이 tensor layout에 민감하지 않거나 변경 가능할 것

적용 효과

  • memory copy 제거
  • GPU bandwidth 절감
  • Multi-head attention block에서 head 간 layout 변환 overhead 제거

3.2.2 Shape Inference

기법 개요

  • static computation graph에서 tensor shape를 사전에 계산하여 dynamic shape 연산을 제거함
  • 불필요한 shape 연산자 제거 및 memory 사전 할당에 유리함

대표 패턴

  • 문제: Unsqueeze, Expand, Reshape가 실행 시점에 shape 계산을 반복 수행
  • 해결: shape 정보를 export 시점에 확정하여 정적 연산자로 고정 → [예시 3.2.3]

적용 조건

  • 입력 shape이 고정되어 있을 것
  • shape 연산 결과가 static하게 계산 가능할 것

적용 효과

  • runtime shape 계산 제거
  • memory buffer 사전 배치 가능
  • backend optimizer가 실행 계획을 더 정확히 수립할 수 있음

3.2.3 Common Subexpression Elimination (CSE)

기법 개요

  • 동일한 연산이 여러 번 수행되는 경우 중복 subgraph를 제거하고 연산 결과를 공유함
  • Transformer 구조에서는 mask 생성, positional embedding 등에서 빈번히 발생

대표 패턴

  • 문제: 동일한 attention mask가 layer마다 반복 생성됨
  • 해결: 한 번 생성된 mask를 모든 layer에서 재사용 → [예시 3.2.4]
  • 문제: sin/cos positional embedding이 block마다 반복 호출됨
  • 해결: 공유 embedding tensor로 graph 내에서 reuse

적용 조건

  • 연산이 pure function일 것 (입력 동일 → 출력 동일, side-effect 없음)
  • 입력이 동일할 것

적용 효과

  • redundant 연산 제거
  • 연산량 및 메모리 사용량 절감
  • graph size 감소, execution trace 단순화

3.2.5 Dataflow-Level Optimization 정리표

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

3.2.6 Dataflow-Level Optimization 예시

[예시 3.2.1] Transpose Pair 제거

문제  
Transformer의 Multi-head Attention 입력 생성 과정에서 다음과 같은 연산 흐름이 존재함:

1. 입력 tensor x의 shape: (B, S, H × D)
2. Linear projection 후:  
   → x_proj = Linear(x) → shape: (B, S, H, D)
3. Head-first layout을 맞추기 위해 transpose 수행:  
   → x_t = Transpose(x_proj, perm=(0, 2, 1, 3)) → shape: (B, H, S, D)
4. Attention 이후, 원래 순서로 되돌리기 위해 다시 transpose 수행:  
   → x_out = Transpose(x_t, perm=(0, 2, 1, 3)) → shape: (B, S, H, D)

즉, 연산 흐름:
x → Linear → Transpose(0,2,1,3) → Transpose(0,2,1,3) → …

문제는 두 Transpose가 서로 역함수 관계가 아님에도 불구하고,  
실제 구현에서는 같은 perm을 반복 호출함으로써 결과적으로 동일 효과를 만들어냄.

더 일반적으로, 다음과 같은 구조가 자주 등장함:

→ Transpose(perm=X) → 연산 → Transpose(perm=inv(X))

하지만 ONNX나 TorchScript로 export 시, 이 상쇄 가능성을 프레임워크가 자동 인식하지 못하고  
각각의 Transpose를 개별 연산으로 고정함 → 쓸모없는 memory 이동 두 번 발생

해결  
- 연산자의 입력/출력 shape와 layout을 비교하여, Transpose pair가 상쇄 가능한 경우 제거
- ONNX optimizer, TensorRT parser 등에서 `eliminate_nop_transpose`, `merge_transpose` pass 수행
- Transpose(perm) → Transpose(inv(perm)) 구조일 때 제거 가능

예시 코드 (ONNX 시각화 기준):

Before:
  x: (B, S, H, D)  
  → Transpose(0, 2, 1, 3)  
  → ScaledDotProductAttention  
  → Transpose(0, 2, 1, 3)  
  → Output

After (최적화 후):
  x: (B, S, H, D)  
  → ScaledDotProductAttention (입력 layout을 맞추도록 변경)  
  → Output

적용 효과  
- GPU memory copy 2회 제거  
- TensorRT 기준, attention block latency 6~9% 감소 (BERT-base, batch=1)  
- Head dimension이 커질수록 layout 전환 cost 증가 → 최적화 효과 증가
[예시 3.2.2] Transpose + MatMul Layout 통합

문제  
PyTorch에서 Linear layer는 내부적으로 다음과 같은 연산으로 표현됨:

  y = x @ W.T + b

이때 ONNX로 export하면 ".T" 연산이 Transpose 연산자로 명시됨.  
그 결과 다음과 같은 subgraph가 생김:

  x: (B, S, D_in)  
  → Transpose(x, perm=(0, 2, 1)) → shape: (B, D_in, S)  
  → MatMul(weight W): shape (D_out, D_in)  
  → Output: shape (B, D_out, S)

문제는, weight가 고정되어 있고 입력도 고정된 layout을 갖는다면  
굳이 x에 Transpose를 적용하지 않고도, W 자체를 바꾸는 방식으로 동일한 연산을 수행할 수 있음.

즉, Transpose + MatMul(x, W)  
= MatMul(x, W^T)  
= MatMul(x, new_W) ← 여기서 new_W는 이미 Transpose 된 weight

하지만 대부분의 프레임워크는  
- 입력 x의 layout을 바꾸기 위해 Transpose를 추가함  
- weight W는 학습 시 layout 그대로 유지함  
→ 결국 Transpose가 runtime에 memory copy를 유발하는 불필요한 연산이 됨

해결  
- 학습이 완료된 모델에 대해 weight W를 미리 transpose하여 "new_W = W.T"로 저장  
- ONNX export 시 Transpose(x) 제거  
- MatMul(x, new_W)로 표현하여 Transpose 없이 동일한 결과 도출

실제 연산 흐름 변경 예:

Before:
  x: (B, S, D_in)  
  → Transpose(x, (0, 2, 1))  
  → MatMul(W: D_out × D_in)  
  → Output

After:
  x: (B, S, D_in)  
  → MatMul(W.T: D_in × D_out)  
  → Output

적용 효과  
- Transpose 연산 제거 → memory 이동 1회 제거  
- MatMul 성능 개선 (layout 고정 시 텐서 연속성 확보)  
- 실제 A100 기준, encoder layer 내 Linear block latency 5~7% 감소
[예시 3.2.3] Static Shape Inference

문제  
Transformer encoder에서 residual connection이나 feed-forward block 이후,  
다음과 같은 shape 관련 연산이 자주 등장함:

1. 입력: x의 shape = (B, S, D)
2. → Unsqueeze(dim=1) → shape: (B, 1, S, D)
3. → Expand(…, H, …) → shape: (B, H, S, D)  # multi-head broadcast
4. → Reshape or View → shape: (B×H, S, D)  
5. → Linear → Output

이러한 Unsqueeze, Expand, Reshape 연산은  
PyTorch에서는 동적으로 처리되어 문제가 되지 않지만,  
ONNX나 TensorRT로 export할 경우 각 단계의 output shape을 실행 시점에 계산함.

특히 다음과 같은 연산이 발생함:
- onnx::Unsqueeze, onnx::Expand, onnx::Reshape 연산자  
- 이들은 shape_tensor를 입력으로 받아 실행 시점마다 shape 계산을 유도함

결과적으로:
- shape 계산을 위한 memory allocation이 발생함  
- dynamic shape 해석이 필요한 경우 runtime fallback이 발생함  
→ 전체 추론 경로에서 오버헤드로 작용

해결  
- Export 전에 입력 tensor의 shape를 고정하고, 연산의 shape 파라미터를 정수 constant로 설정  
- 예를 들어:  
  - torch.reshape(x, (B*H, S, D)) → ONNX에서는 (4, 64, 128)처럼 구체 숫자로 고정  
  - torch.expand(x, (-1, num_heads, -1, -1)) → Expand([1, 8, 64, 128])로 변환  
- ONNX export 시 dynamic_axes를 지정하지 않거나, static shape로 변환 스크립트 적용

ONNX 모델 내 예시:

Before:
  x: dynamic shape = (B, S, D)  
  → shape(x) = runtime 계산  
  → Reshape(x, shape) = runtime 연산

After:
  x: static shape = (4, 64, 128)  
  → Reshape(x, [4, 64, 128]) = compile-time 처리

적용 효과  
- 모든 shape 연산을 static tensor로 변환 가능  
- runtime shape 해석 제거 → ONNXRuntime, TensorRT에서 그래프 실행 속도 개선  
- memory preallocation 가능  
- 실험 결과: reshape-heavy layer에서 latency 10~20% 감소 (BERT-Large 기준, batch=1)
[예시 3.2.4] Attention Mask 중복 제거 (Common Subexpression Elimination)

문제  
Transformer 구조에서 모든 layer는 동일한 attention mask를 사용함.  
하지만 일반적인 모델 export 경로에서는 각 layer마다 독립적으로 mask 생성 연산이 포함됨.

실제 구조 예:

1. 입력: input_ids → shape = (B, S)
2. → attention_mask = (input_ids != pad_token_id).to(dtype)  
3. → attention_mask → Expand / Unsqueeze → shape: (B, 1, 1, S)
4. → 각 attention head에서 반복 사용됨

그러나 모델을 ONNX로 export하거나 TorchScript로 tracing할 경우,  
각 layer가 스스로의 연산에 필요한 attention_mask를 별도로 복제해서 소유하는 형태로 그래프에 포함됨.

결과적으로:
- 동일한 입력으로 동일한 연산 (== mask 생성)이 여러 번 수행됨
- 동일한 tensor가 여러 개의 버전으로 그래프에 존재함 (tensor_23, tensor_49, tensor_62 등)
- 각 head 혹은 layer가 독립된 attention_mask를 갖고 있어 중복 연산 발생

해결  
- Subgraph 패턴이 동일하고 입력이 같다면, 해당 연산을 재사용하도록 graph를 rewrite  
- ONNX optimizer에서 CSE (Common Subexpression Elimination) 패스를 적용  
- 또는 export 시 mask를 미리 생성하여 constant input으로 주입

Before:
  input_ids → mask_fn → attn_mask_1  
            → mask_fn → attn_mask_2  
            → mask_fn → attn_mask_3 ...

After:
  input_ids → mask_fn → shared_attn_mask  
                          ↓     ↓     ↓  
                         all attention blocks

적용 조건  
- mask 생성 연산이 pure function일 것 (e.g., deterministic, side-effect 없음)
- input이 동일할 것

적용 효과  
- 중복 연산 제거 → CPU preprocessing 시간 감소  
- 그래프 tensor 수 감소  
- ONNXRuntime 기준 encoder graph size 10~15% 감소  
- Transformer encoder inference latency 평균 5~8% 개선

3.3 Memory & Resource Optimization

  • Memory & resource optimization은 불필요한 memory allocation, copy, duplication을 줄이고, 전체 메모리 footprint를 감소시켜 inference 안정성과 효율성을 확보하는 것을 목표로 함.
  • Transformer 구조는 sequence length, hidden size, activation tensor 크기 등의 영향으로 memory pressure가 크며, memory 최적화 여부가 곧 long-context 처리와 batch scalability를 결정함.
  • 네가지 주요 memory-level 기법: memory reuse, inplace computation, weight sharing, mixed precision.

3.3.1 Memory Reuse

기법 개요

  • 중간 연산 결과 tensor의 생존 기간(lifetime)을 분석하여, 더 이상 사용되지 않는 buffer를 다음 연산에 재활용함
  • static graph에서는 live-range 분석 기반의 memory planning이 적용됨

대표 패턴

  • 문제: Linear → GELU → Linear → Add → LayerNorm 구조에서, GELU 입력은 이후 사용되지 않지만 별도 메모리에 유지됨
  • 해결: 해당 tensor buffer를 Linear2 또는 Add 연산에 재할당 → [예시 3.3.1]

적용 조건

  • overwriting이 semantics를 바꾸지 않을 것
  • 해당 tensor가 이후 연산에 참조되지 않을 것

적용 효과

  • 불필요한 allocation 제거
  • peak memory usage 최대 30~35% 절감
  • long-sequence 입력에서 OOM 완화

3.3.2 Inplace Computation

기법 개요

  • 연산 결과를 새로운 buffer에 쓰는 대신, 입력 tensor를 overwrite하여 메모리 재사용을 수행함
  • PyTorch, ONNX, TensorRT 등에서 부분 지원됨

대표 패턴

  • 문제: LayerNorm(x)의 결과를 별도 메모리에 저장하여 메모리 중복 발생
  • 해결: LayerNorm(x, out=x) 형태로 inplace 처리 → [예시 3.3.2]

적용 조건

  • 입력 tensor가 이후 연산에서 재사용되지 않을 것
  • 연산이 pure function일 것

적용 효과

  • memory copy 제거
  • memory footprint 평균 10~15% 감소

3.3.3 Weight Sharing / Buffer Folding

기법 개요

  • 여러 연산 블록에서 동일한 constant 또는 weight가 반복 사용될 때, 이를 복사하지 않고 단일 메모리 참조로 folding 처리함

대표 패턴

  • 문제: positional embedding이 block마다 중복 포함되거나, W_k = W_q 구조에서 weight가 중복 저장됨
  • 해결: 동일 weight를 공유 buffer로 folding → [예시 3.3.3]

적용 조건

  • parameter 또는 constant가 완전히 동일할 것
  • inference 전용 경로에서 gradient 계산 불필요할 것

적용 효과

  • model size 및 메모리 usage 감소 (1~3%)
  • weight duplication 제거
  • load time 및 GPU memory 초기화 시간 절감

주의사항: Constant Folding과의 차이점

항목 Weight Sharing / Buffer Folding (3.3.3) Constant Folding
목적 동일 tensor를 여러 번 복제하지 않고 공유 고정된 연산 결과를 미리 계산하여 연산 자체 제거
적용 대상 positional embedding, shared weight 등 add, mul, reshape, cast 등 계산 가능한 연산
변경 대상 tensor의 메모리 참조 방식 연산 노드 자체를 제거하고 상수로 치환
주요 효과 model size 감소, memory footprint 절감 runtime 연산량 감소, latency 개선
수행 방식 동일 tensor를 단일 buffer로 연결 연산 결과를 미리 계산하여 graph에 삽입

3.3.4 Mixed Precision

기법 개요

  • FP32 대신 FP16 또는 BF16 타입으로 연산을 수행하여 memory 사용량과 연산 비용을 동시에 절감함
  • 대부분의 Transformer 연산은 저정밀도에서도 안정적으로 수행 가능

대표 패턴

  • 문제: 모든 연산을 FP32로 수행 시 memory 과다 사용, 연산 성능 저하
  • 해결: FP16 변환 + LayerNorm, Softmax는 FP32 유지 → [예시 3.3.4]

적용 조건

  • overflow/underflow에 민감하지 않을 것
  • AMP 또는 loss scaling을 통해 수치 안정성 확보할 것

적용 효과

  • GPU memory usage 약 40~50% 절감
  • latency 최대 2배 개선
  • A100, H100 등 mixed precision 최적화 아키텍처에서 큰 효과

주의 사항

  • LayerNorm과 Softmax는 내부적으로 매우 작은 값 또는 매우 큰 값을 다루며,  FP16에서는 rounding error, underflow, overflow가 발생할 수 있음  
  • 예를 들어 Softmax의 exp/log 계산은 작은 차이가 크게 증폭될 수 있고, LayerNorm의 variance 계산은 정밀도 손실에 민감함  
  • 따라서 이들 연산은 일반적으로 FP32로 강제 유지하거나,  loss scaling 등의 기법으로 안정성을 확보한 후에만 FP16 적용 권장

3.3.5 Memory & Resource Optimization 정리표

Optimization Technique 주요 대상 패턴 적용 효과
Memory Reuse 중간 tensor live-range 재활용 allocation 절감, memory peak 최대 35% 절감
Inplace Computation LayerNorm, Dropout, Residual Add memory 복사 제거, footprint 감소
Weight Sharing / Folding W_q = W_k, embedding buffer 반복 제거 model size 감소, load time 개선
Mixed Precision FP32 → FP16/BF16 변환, critical op만 FP32 유지 memory 절반 사용, latency 최대 2배 개선

3.3.6 Memory & Resource Optimization 예시

[예시 3.3.1] FFN block 내 memory reuse

문제  
Transformer의 feed-forward block은 다음과 같은 연산 흐름을 가짐:

x → Linear1(x) → y1
→ GELU(y1)     → y2
→ Linear2(y2)  → y3
→ Add(y3, x)   → residual
→ LayerNorm(residual)

이 구조에서 y1 (Linear1의 출력)은 GELU를 거친 후 더 이상 사용되지 않음.  
그러나 대부분의 graph에서는 y1을 별도 tensor로 유지하고 있어 불필요한 memory 사용이 발생함.

결과적으로:
- 중간 tensor가 연산 이후에도 메모리에 남아 있음
- 메모리 peak 사용량이 증가함
- long-context 입력에서는 OOM 위험 증가

해결  
- live-range analysis를 통해 y1의 생존 범위를 확인하고  
- Linear2 또는 Add 연산이 y1의 메모리 buffer를 재사용하도록 메모리 할당 최적화  
- TensorRT, ONNXRuntime 등에서 static memory planning 기반으로 적용 가능

적용 조건  
- 해당 tensor가 이후 연산에서 참조되지 않을 것  
- overwrite가 연산 semantics에 영향을 주지 않을 것

적용 효과  
- memory reuse를 통해 peak memory usage 최대 30% 절감  
- batch size 혹은 sequence length scaling 가능성 확보  
- inference 시 memory footprint 안정화
[예시 3.3.2] LayerNorm inplace 처리

문제  
LayerNorm 연산은 입력과 출력의 shape가 동일하며,  
추론 시에는 출력 tensor를 별도 buffer에 저장할 필요 없이 입력을 overwrite할 수 있음.

그러나 일반적인 모델에서는 다음과 같은 형태로 처리됨:

x → LayerNorm(x) → y  
이후 y를 다음 연산으로 전달

이 경우 x와 y가 동일한 shape임에도 불구하고  
두 개의 memory block을 유지하게 되어 메모리 비효율 발생.

해결  
- x가 이후에 사용되지 않는 경우, LayerNorm을 inplace 연산으로 변경  
- PyTorch: `inplace=True` 옵션 적용  
- TorchScript 및 ONNX export 시, Graph Rewrite 또는 Manual Optimization으로 적용

적용 조건  
- 입력 tensor가 이후 연산에서 참조되지 않을 것  
- LayerNorm이 deterministic하고 side-effect 없는 함수일 것

적용 효과  
- output buffer 제거  
- layer 당 memory footprint 5~10% 감소  
- 전체 모델 기준 memory 절감 효과 누적 발생
[예시 3.3.3] Positional Embedding Weight Sharing

문제  
Transformer 구조에서는 sin/cos positional embedding 또는 learnable embedding이  
각 layer에서 반복적으로 사용되며, 동일한 tensor가 여러 위치에 복제됨.

특히 다음과 같은 구조에서 문제가 발생함:

1. embedding = SinCos(position_ids)
2. encoder_layer_1 → embedding  
3. encoder_layer_2 → embedding (복제)  
4. encoder_layer_3 → embedding (복제) ...

ONNX export 또는 static tracing에서는 각 layer에 embedding이 복사됨  
→ 동일한 tensor가 memory 상에 여러 번 저장됨  
→ model size 증가, load time 증가

해결  
- shared constant buffer로 embedding을 graph 상에 단일화  
- weight folding pass를 통해 constant reference로 변경  
- TorchScript, TensorRT, ONNXRuntime에서 buffer folding 최적화 적용

적용 조건  
- embedding tensor가 모든 layer에서 identical할 것  
- inference-only 경로일 것 (gradient 불필요)

적용 효과  
- model size 최대 2~3% 감소  
- GPU memory 상 constant duplication 제거  
- loading time 및 initialization overhead 감소
[예시 3.3.4] Mixed Precision 적용 (FP16)

문제  
Transformer 모델을 FP32로 inference할 경우,  
모든 tensor가 4바이트로 유지되어 memory usage가 높고  
Tensor Core와 같은 하드웨어 가속기를 활용하지 못해 latency가 증가함.

예시 구조:

1. Linear(x) → y  
2. Add(y, residual)  
3. LayerNorm(y)  
4. MatMul(q, k^T) / sqrt(d_k)

이 모든 연산이 FP32로 수행되면 memory bandwidth와 연산량이 증가함.  
특히 large model + long sequence 환경에서 GPU memory bottleneck 유발.

해결  
- 연산 전반을 FP16/BF16으로 변환  
- LayerNorm, Softmax는 FP32 유지 또는 loss scaling 적용  
- PyTorch AMP, ONNX precision pass, TensorRT builder에서 지원

적용 조건  
- 모델이 overflow/underflow에 민감하지 않을 것  
- AMP, loss scaling, mixed-precision-safe 연산자가 확보될 것

적용 효과  
- tensor size 절반 → GPU memory usage 40~50% 감소  
- Tensor Core 활용 → 연산 속도 최대 2배 향상  
- A100, H100 등 mixed precision 최적화 GPU에서 peak 효율 달성

3.4 Execution Graph Optimization

  • Execution graph 최적화는 전체 연산 흐름을 대상으로**, 불필요한 분기, 중복 노드, 사용되지 않는 출력** 등을 제거하고 반복적인 서브그래프 구조를 재구성하여 실행 효율을 향상시키는 기법임.
  • Transformer 모델은 고정된 구조를 반복하므로, subgraph pattern 최적화 및 control flow 단순화의 효과가 큼.
  • 네 가지 주요 execution graph 기법: control flow simplification, redundant node pruning, dead output elimination, subgraph folding

3.4.1 Control Flow Simplification

기법 개요

  • 모델 내의 if, Loop, Where, Select 등 제어 흐름 연산 중, 실행 시 항상 같은 조건을 갖는 분기를 제거하여
    정적 연산 그래프로 단순화하는 기법.
  • decoder-only 설정, use_cache, train/eval 구분 등의 조건이 대표적인 대상이며,
    분기 경로가 고정되어 있을 경우 그래프에 불필요한 control flow 연산이 잔존함.

대표 패턴

  • 문제: inference graph에 if (decoder_only) 조건이 포함되어 cross-attention 여부를 선택하는 분기가 있음
  • 해결: decoder_only=True로 고정되어 있을 경우, 해당 분기를 제거하고 단일 경로로 고정 → [예시 3.4.1]

적용 조건

  • 조건 분기가 상수 조건일 것 (예: decoder_only=True, use_cache=False 등)
  • 분기 대상 연산이 deterministic하고 side-effect 없을 것

적용 효과

  • 불필요한 control flow 노드 제거 → 실행 경로 단순화
  • ONNX, TorchScript 등에서 그래프 최적화 패스 적용 가능
  • cross-attention, masking, layer skip 등에서 latency 5~10% 개선


3.4.2 Redundant Node Pruning

기법 개요

  • 의미 없는 노드(Identity, Cast(same dtype), Dropout(p=0), No-op 등)를 그래프에서 제거하여 노드 수를 줄이고 실행 흐름을 간결화함
  • export 및 tracing 시 생성된 불필요한 중간 노드들이 대상

대표 패턴

  • 문제: x → Cast(float32 → float32) → Identity → x'
  • 해결: 연산 결과가 입력과 동일한 경우 해당 연산자를 삭제 → [예시 3.4.2]

적용 조건

  • 해당 연산이 입력과 출력에 변화가 없을 것
  • side effect 없는 연산일 것

적용 효과

  • ONNX 기준 노드 수 5~20% 감소
  • model size 소폭 감소
  • compile 속도 및 graph 분석 시간 단축

3.4.3 Dead Output Elimination

기법 개요

  • 모델의 출력 중 실제 downstream에서 사용되지 않는 output tensor를 제거하는 기법
  • decoder의 hidden_states, attention_scores, past_key_values 등이 대표적

대표 패턴

  • 문제: 모델 출력이 (logits, hidden_states, attentions)로 정의되었으나 실제 사용은 logits만 함
    해결: 사용되지 않는 출력에 대한 연산 경로 및 tensor 제거 → [예시 3.4.3]

적용 조건

  • output이 downstream task에서 참조되지 않을 것
  • side-effect 없는 계산일 것

적용 효과

  • memory usage 감소
  • encoder-decoder 모델 기준 output projection block latency 8~12% 감소
  • 불필요한 tensor copy 제거

3.4.4 Subgraph Folding & Pattern Matching

기법 개요

  • Transformer 구조에서 반복적으로 나타나는 연산 조합(QKV + MatMul + Softmax + MatMul + Add 등)을 subgraph 단위로 인식하고, 하나의 커스텀 연산(fused op) 또는 최적화된 블록으로 대체하는 기법

대표 패턴

  • 문제: attention block이 개별 연산으로 표현되어 연산 수 증가 및 메모리 이동 많음
    해결: subgraph 패턴 매칭을 통해 FusedAttention 등으로 치환 → [예시 3.4.4]

적용 조건

  • 반복적인 연산 구조가 동일한 순서, shape으로 등장할 것
  • backend에서 해당 subgraph 패턴을 지원할 것 (TensorRT, TVM, ONNXRuntime EP 등)

적용 효과

  • 연산 수 5~10개 → 1개로 병합
  • memory 이동 제거, context switching 절감
  • attention block latency 최대 30~40% 감소

3.4.5 Execution Graph Optimization 정리표

Optimization Technique Representative Pattern 적용 효과
Control Flow Simplification if, Select, Dropout(training) 분기 제거, 실행 경로 단순화
Redundant Node Pruning Identity, Cast(x→x), Dropout(p=0) 불필요 연산 제거, 노드 수 감소
Dead Output Elimination hidden_states, past_key_values 출력 관련 연산 제거, 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 구조에서 다음과 같은 조건 분기 존재함:  
    if decoder_only:
        cross_attention = None  
    else:
        cross_attention = cross_attention_block(...)  
- 이 구조가 tracing 또는 export 시 ONNX의 If 노드로 변환됨  
- 실제로는 decoder_only=True로 고정되어 있어 분기 경로가 항상 하나로 고정됨  
- 그럼에도 그래프에는 If, Constant, Identity, unused branch가 모두 포함됨

해결  
- decoder_only 조건이 static하게 True로 설정되어 있다면, 분기 자체를 제거하고 단일 경로로 고정  
- 사용되지 않는 branch 내 연산은 DCE 또는 dead branch elimination으로 제거  
- ONNX에서는 eliminate_deadend / remove_unused_branch 패스 적용

적용 조건  
- 조건문이 static한 상수 조건일 것 (e.g., decoder_only=True)  
- 제거되는 분기 경로가 side-effect 없이 무시 가능할 것

적용 효과  
- If, Constant, Identity 등 control flow 관련 노드 제거  
- cross-attention 관련 unused 서브그래프 제거 → encoder-decoder latency 감소  
- 그래프 내 불필요한 분기 구조 제거 → compile 시간 및 실행 안정성 향상
[예시 3.4.2] Redundant Node Pruning - Identity & Cast 제거

문제  
- PyTorch → ONNX export 또는 TorchScript tracing 과정에서,  
  내부 연산자의 추상화/추적을 위해 Identity, Cast 같은 연산이 삽입됨  
- 예를 들어, dynamic shape 추적, dtype 정합성 확인, tracing 경계 표시 등의 목적으로  
  다음과 같은 불필요한 노드가 생성됨:  
    x → Cast(dtype=float32) → Identity → y  
- 이들은 실제 연산값에 영향을 주지 않지만, 그래프 상에서는 별도 노드로 고정되어  
  실행 시 memory 이동 및 연산자 호출을 유발함  
- 특히 export 중간에 torch.tensor(), .to(), .clone(), .detach() 등의 함수가  
  Identity 또는 redundant Cast로 변환되는 경우가 많음

해결  
- ONNX optimizer에서 eliminate_identity, eliminate_nop_cast 패스를 통해 불필요한 노드 제거  
- TorchScript, torch.fx, TensorRT 등에서도 simplify_graph, remove_nop_nodes 패스를 통해 제거 가능

적용 조건  
- Identity: 입력과 출력이 동일한 tensor일 것  
- Cast: 입력과 출력 dtype이 동일할 것 (예: float32 → float32)  
- downstream 연산이 dtype/layout 변경에 의존하지 않을 것

적용 효과  
- 연산자 수 5~15% 감소 → 모델 최적화 및 export 성능 향상  
- 그래프 크기 및 분석 시간 감소  
- memory copy 및 launch overhead 최소화 → latency 소폭 개선
[예시 3.4.3] Dead Output Elimination - hidden_states 제거

문제  
- Transformer 기반 모델(HuggingFace 등)은 다음과 같이 여러 출력을 리턴함  
    return {
        "logits": logits,
        "hidden_states": all_hidden_states,
        "attentions": all_attentions
    }  
- 이 중 실제 downstream에서는 logits만 사용되는데도,  
  ONNX export 또는 TorchScript tracing 시 모든 출력이 그래프에 고정됨  
- 특히 hidden_states는 각 layer의 출력값을 누적 저장하므로  
  전체 layer 수에 비례해 메모리 사용량과 연산 오버헤드가 증가함  
- export 시 output signature를 명시하지 않으면,  
  TensorRT, ONNXRuntime 등에서도 해당 출력을 위한 subgraph가 유지됨

해결  
- export 시 output signature를 명시적으로 제한하여  
  사용하지 않는 출력 (e.g. hidden_states, attentions)을 제외함  
- ONNX에서는 output filtering 또는 `remove_unused_outputs`, `eliminate_unused_initializer` 패스 사용  
- TensorRT에서는 build 단계에서 unused output 제거가 자동 적용되지만,  
  명시적 출력을 제한하면 최적화 범위가 훨씬 커짐  
- TorchScript에서는 wrapper 함수로 출력 필터링 후 tracing 수행

적용 조건  
- 제거 대상 출력이 실제 downstream에서 참조되지 않을 것  
- 해당 출력 경로가 deterministic하고 side-effect 없을 것

적용 효과  
- layer별 hidden state 저장 연산 제거 → memory usage 절감  
- decoder 모델 기준, output 관련 연산 latency 10~12% 감소  
- 그래프 크기 감소 (encoder-decoder 기준 최대 20%)  
- TensorRT 빌드 시 subgraph 크기 축소 → engine 크기 및 빌드 시간 단축  
- ONNXRuntime에서도 불필요한 tensor copy 제거 → GPU memory footprint 감소
[예시 3.4.4] Subgraph Folding - Fused Multi-Head Attention

문제  
- Transformer의 Self-Attention 블록은 다음과 같은 subgraph로 구성됨  
    x → Linear_q, Linear_k, Linear_v  
      → MatMul(q, k^T) → Scale → Softmax → Dropout  
      → MatMul(..., v) → Output projection  
- 각 연산이 개별 노드로 존재하면 memory 이동, kernel launch, tensor copy가 반복 발생  
- layer가 깊어질수록 해당 패턴이 반복되며 latency 누적됨  
- ONNX export 시에도 위 연산들이 각각 독립된 연산으로 고정되어 subgraph 크기가 커짐

해결  
- 위 연산 패턴을 subgraph matcher로 인식하고 FusedMultiHeadAttention 연산으로 folding 처리  
- TensorRT에서는 MHA plugin이 존재하며, builder에서 subgraph 패턴을 자동 감지하고 하나의 fused op로 최적화  
- HuggingFace Optimum, ONNX GraphSurgeon 등에서도 동일한 folding 패턴 적용 가능  
- TorchScript에서는 pattern matching을 통한 relay fusion 또는 IR 변환 수행

적용 조건  
- attention 구조가 고정된 순서, shape, dtype으로 구성되어 있을 것  
- backend가 해당 MHA subgraph 패턴을 지원하고, 조건을 만족할 것  
- query/key/value가 동일한 head 수와 hidden dim을 가질 것

적용 효과  
- 연산자 수: 8~10개 → 1개로 병합  
- memory copy 제거, kernel launch 수 감소  
- attention block latency 최대 30~40% 감소 (TensorRT A100 기준, BERT-base)  
- GPU compute unit utilization 개선 → throughput 향상  
- TensorRT engine size 감소, 컴파일 시간 단축

3.5 Precision Reduction

  • Precision Reduction은 연산자의 데이터 타입을 float32(FP32)에서 float16(FP16), bfloat16(BF16), int8 등으로 축소하여 연산량과 memory usage를 동시에 줄이는 기법임
  • Transformer 구조는 대부분의 연산이 low-precision에서도 안정적으로 수행되며, 대규모 모델의 inference latency는 대부분 precision 및 memory bandwidth에 의해 결정됨
  • 세 가지 주요 정밀도 최적화 기법: Mixed Precision (FP16/BF16), Dynamic Quantization, Static Quantization

3.5.1 Mixed Precision (FP16/BF16)

기법 개요

  • FP32 연산을 FP16 또는 BF16으로 축소하여 memory 사용량과 연산량을 줄이고, Tensor Core 등 low-precision 가속 유닛을 활용하는 방식
  • Transformer 구조는 대부분의 연산에서 precision loss에 강건하여 mixed precision 적용이 용이함

적용 방식

  • 대부분의 연산을 FP16/BF16으로 전환하되, Softmax, LayerNorm 등은 FP32로 유지
  • AMP(Automatic Mixed Precision) 방식 또는 수동 precision 조정 방식 존재
  • TensorRT, PyTorch AMP, ONNX precision override 등에서 지원됨

주의사항

  • Softmax와 LayerNorm은 FP16에서 underflow, overflow 발생 가능성 존재
  • exp, var, sqrt 등 민감한 연산은 FP32 precision 유지 권장
  • 대부분의 backend는 해당 연산을 자동으로 FP32 fallback 처리함

3.5.2 Dynamic Quantization

기법 개요

  • 모델 weight는 export 시점에 정수(INT8)로 변환하고, activation은 runtime에 quantization 처리
  • activation 값은 실행 시 실시간으로 Quantize → Dequantize 연산을 통해 정수로 변환됨
  • quantization parameter(scale, zero-point)는 runtime에서 입력값 분포를 기반으로 동적으로 계산

Quantize-Dequantize 구조

  • 연산 예시:
    float_input → Quantize(scale, zero_point) → int8_input
    int8_input → Dequantize(scale, zero_point) → quantized_float_input
  • 실제로는 weight만 INT8로 고정되고, activation은 연산 직전에 quant-dequant 연산을 거쳐 사용됨

장점

  • calibration 데이터 불필요 → export가 간편함
  • 정확도 손실 최소화 → conservative quantization

한계

  • activation quantization이 runtime에 수행되므로, GPU에서는 quantized kernel로 변환되지 않음
  • 대부분의 GPU backend는 dynamic quantization path를 지원하지 않음
  • CPU inference (ONNXRuntime, DNNL, QNNPack)에서만 latency 이득 존재

3.5.3 Static Quantization

기법 개요

  • weight, activation 모두를 INT8로 고정하며, quantization parameter를 export 전에 사전 계산
  • 이 방식은 GPU에서도 INT8 kernel 경로를 사용할 수 있어 성능 최적화에 효과적임
  • 단, activation의 통계치를 사전 수집해야 하므로 calibration dataset이 필요

적용 방식

  • calibration 데이터를 통해 각 연산별 입력/출력 tensor의 min/max 범위 수집
  • ONNX 또는 PyTorch에서 Quantize-Dequantize(QDQ) 연산을 그래프에 삽입
  • TensorRT, ONNXRuntime, OpenVINO 등에서 정적 INT8 kernel로 최적화 수행

GPU 최적화

  • activation 범위가 고정되어 있으므로 GPU에서 quantized 연산으로 실행 가능
  • TensorRT builder는 INT8 enable 시 자동으로 MHA, MatMul, Linear 등을 INT8 kernel에 매핑
  • Transformer 구조 전체를 INT8 graph로 실행 가능

Optimization Technique 적용 대상 전제 조건 주요 효과
Mixed Precision (FP16/BF16) 대부분 연산자 (GPU) AMP 적용 가능, overflow 회피 필요 latency 1.5~2배 개선, memory 절반
Dynamic Quantization Linear, MatMul 등 (CPU) calibration 불필요, GPU 미지원 memory 절감, CPU latency 개선
Static Quantization (INT8) Linear, Attention 등 (GPU) calibration set 필요, 정적 graph model size 75% 절감, GPU 최적화 가능
  • Transformer 구조는 attention, feedforward, projection 등에서 low-precision에 강건하므로,
    대부분의 최적화 기법이 적용 가능함

3.5.4 Precision Reduction 예시

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

문제  
- Transformer 모델을 FP32로 실행할 경우 모든 tensor가 4바이트로 유지됨  
- GPU memory bandwidth가 병목이 되며, Tensor Core 등 low-precision 가속기를 활용하지 못함  
- 특히 Linear, MatMul, Attention, Projection 등 고비용 연산에서 latency가 증가함  
- long sequence 또는 batch size 증가 시 GPU memory 부족 위험도 커짐

해결  
- 연산 전반을 FP16 또는 BF16으로 변환하여 tensor 크기 절반으로 축소  
- LayerNorm, Softmax 등 수치 불안정성이 큰 연산은 FP32 precision 유지  
- PyTorch에서는 autocast 기반 AMP 적용, ONNX는 precision override 또는 manual cast-insertion 방식 사용  
- TensorRT에서는 builder flag (enable_fp16) 설정 시 자동으로 FP16 kernel을 할당함 (calibration 불필요)

적용 조건  
- 모델이 underflow, overflow에 민감하지 않을 것  
- AMP 또는 cast-safe 연산자 구성이 확보되어 있을 것  
- 수치 불안정성이 큰 연산에 selective precision 적용이 가능할 것

적용 효과  
- tensor size 절반 → GPU memory usage 40~50% 감소  
- Tensor Core 활용 → 연산 속도 최대 2배 향상 (A100 기준)  
- latency 감소 + batch size 확장 + memory footprint 절감 → overall throughput 증가  
- mixed precision 최적화 아키텍처에서 peak performance 달성

주의사항  
- LayerNorm은 분산(variance) 계산 시 매우 작은 값의 제곱과 평균을 다루기 때문에,  
  FP16 환경에서는 underflow, rounding error로 정밀도 손실이 발생할 수 있음  
- Softmax는 입력 logit 차이가 클 경우 exponentiation(exp)이 급격히 커지거나 작아져  
  FP16의 표현 범위를 넘어서 overflow / underflow 위험 존재  
- 이러한 연산을 FP16으로 그대로 실행할 경우 output이 NaN, zero 또는 saturated 값으로 왜곡됨  
- 일반적으로 AMP 또는 manual override를 통해 해당 연산만 FP32로 고정함  
- 일부 backend (TensorRT, DeepSpeed, Megatron)에서는 Softmax, Norm 계열 연산을 자동 FP32 처리하도록 설계되어 있음
[예시 3.5.2] Dynamic Quantization 적용 (INT8)

문제  
- Transformer 모델을 FP32로 실행할 경우, weight와 activation 모두 메모리 사용량이 큼  
- 특히 Linear, MatMul 블록이 전체 모델의 연산량과 memory footprint의 대부분을 차지  
- edge 또는 CPU 환경에서 latency와 memory efficiency 확보가 어려움

해결  
- weight를 export 시점에 INT8로 quantize  
- activation은 runtime에 실시간으로 quantize-dequantize 처리  
- 연산은 float domain에서 수행되지만, weight가 정수화되어 memory 사용량 감소  
- ONNX export 시 Linear, MatMul 연산자에 QDQ (Quantize-Dequantize) subgraph 삽입됨

예시 구조  
1. float_input → Quantize(scale_1, zp_1) → int8_input  
2. int8_input × int8_weight → int32_accum  
3. int32_accum → Dequantize(scale_2, zp_2) → float_output  
4. 이후 연산자에 float_output 전달

Quant-dequant가 필요한 이유  
- 대부분의 연산자(MatMul 등)는 float32 전용으로 구현되어 있음  
- int8로 직접 연산 불가하므로, weight만 정수화하고 activation은 runtime에 다시 float로 복원  
- 기존 연산자 path를 유지하면서 일부 정수화된 tensor만 도입 가능

적용 조건  
- Linear, MatMul 중심 구조일 것  
- activation의 clipping이나 dynamic range 추정이 불안정하지 않을 것  
- GPU에서 INT8 연산이 필요하지 않을 것

적용 효과  
- weight memory usage 약 75% 감소  
- PyTorch 및 ONNXRuntime CPU EP 기준 latency 약 20~40% 개선  
- export 간단 (calibration 필요 없음), 정확도 손실 매우 낮음

한계  
- activation이 float domain으로 복원되므로 연산 자체는 여전히 FP32  
- GPU에서는 QDQ 구조가 최적화되지 않아 latency 개선 효과 없음  
- TensorRT, OpenVINO 등 GPU/FPGA backend에서는 dynamic quantization 미지원
[예시 3.5.3] Static Quantization 적용 (INT8)

문제  
- Transformer 모델을 FP32로 실행할 경우, weight와 activation 모두 메모리 사용량과 연산량이 큼  
- 특히 attention block, feedforward, embedding 등 고비용 연산이 누적되어 inference latency가 증가  
- GPU에서 INT8 연산을 사용하려면 activation의 범위를 미리 알아야 하나, dynamic 방식으로는 불가능

해결  
- calibration dataset을 통해 각 연산자의 입력과 출력 tensor의 min/max 범위 측정  
- 측정된 범위 기반으로 각 tensor에 대해 scale, zero-point를 고정  
- ONNX에서는 QuantizeLinear, DequantizeLinear 연산을 삽입한 QDQ 패턴으로 export  
- TensorRT, OpenVINO, QNN EP 등은 이 정적 정보를 바탕으로 INT8 kernel을 매핑

예시 구조  
1. float_input → Quantize(scale_1, zp_1) → int8_input  
2. int8_input × int8_weight → int32_accum  
3. int32_accum → Dequantize(scale_2, zp_2) → float_output  
4. 이후 연산자도 QDQ 구조를 유지하거나 일부 float 연산으로 fallback

calibration이 필요한 이유  
- activation의 dynamic range가 runtime에 따라 달라짐  
- 정적 kernel을 사용하려면 이 범위를 미리 고정해야 함  
- calibration set을 통해 대표적인 입력 분포를 수집하고, 범위 기반 scale/zp를 고정  
- 정확한 calibration이 없으면 clipping 또는 precision 손실로 인해 accuracy 하락 가능

적용 조건  
- 정적 입력 크기 (static shape)를 갖는 모델일 것  
- calibration set이 실제 inference 입력을 충분히 대표할 것  
- QDQ 패턴을 지원하는 backend (TensorRT, ONNXRuntime QNN 등) 사용 가능할 것

적용 효과  
- 모델 크기 최대 75% 감소 (weight + activation 모두 8bit)  
- 연산량 4배 감소 (FP32 대비)  
- TensorRT 기준, encoder layer latency 최대 2.5배 개선  
- GPU memory usage 감소 + INT8 kernel 가속 경로 활성화

주의사항  
- activation의 clipping 범위가 부정확하면 accuracy drop 가능  
- attention logit, residual add 등은 수치 손실에 민감할 수 있어 per-channel quantization 권장  
- 일부 연산자(MatMul, Softmax 등)는 hybrid precision으로 유지하는 것도 고려 가능

3.6 Transformer-Specific Optimization

  • Transformer 계열 모델은 attention, residual, projection block 등 고정된 반복 구조를 가지므로,
    해당 구조에 특화된 subgraph 최적화 기법을 적용하면 일반 최적화보다 더 큰 성능 개선 효과를 얻을 수 있음
  • 특히 Multi-Head Attention, Rotary Embedding, LayerNorm 등 반복 구조는 연산 패턴이 고정되어 있어 최적화 대상이 되기 좋음
  • 세가지 주요 Transformer-Specific 최적화 기법: Fused Multi-Head Attention, Rotary Embedding Precomputation, KV Caching

3.6.1 Fused Multi-Head Attention

기법 개요

  • Q, K, V projection, head split, scaled dot-product, softmax, weighted sum, head concat 등
    attention 연산 전체를 하나의 fused kernel로 처리하는 최적화 기법
  • 일반적인 attention 구현은 각 단계가 별도 연산자(operator)로 분리되어 kernel 호출 및 memory 이동이 많음
  • TensorRT, FlashAttention, ONNXRuntime 등은 이를 하나의 fused op로 처리하는 최적화 경로를 제공함

적용 방식

  • QKV projection weight를 하나로 합친 후, fused attention plugin 호출
  • Softmax, scale, dropout 등도 내부적으로 포함되며, 필요 시 masking까지 fused됨

적용 효과

  • kernel 호출 수 감소 → launch overhead 절감
  • intermediate tensor memory 제거 → memory usage 감소
  • A100 기준, attention block latency 최대 2.3~3.1배 개선 (TensorRT fused mha plugin 기준)

3.6.2 Rotary Embedding Precomputation

기법 개요

  • Rotary positional embedding(RoPE)은 각 토큰 위치에 대해 sin/cos 연산을 수행함
  • 이 연산은 deterministic하고 입력과 무관하므로, runtime이 아닌 export 시점에 미리 계산 가능

적용 방식

  • RoPE용 sin, cos 테이블을 미리 계산하여 embedding tensor로 삽입
  • runtime에는 단순 indexing만 수행
  • constant folding 또는 static buffer 삽입 방식으로 구현됨

적용 효과

  • trigonometric 함수 제거
  • rotary embedding 연산 latency 60~70% 절감
  • token 생성 속도 안정화

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

기법 개요

  • GPT 계열 decoder 모델은 이전 시점의 K/V를 매번 재계산하지 않고 캐시하여 재사용
  • decoding loop에서 반복적으로 context를 쌓아가는 구조이기 때문에 매우 중요한 최적화

적용 방식

  • 1-step decoding 시점에서 생성된 key, value tensor를 별도 버퍼에 저장
  • 다음 시점 입력에서는 query만 새로 계산하고, 기존 K/V는 그대로 활용
  • TensorRT, FasterTransformer 등은 이를 GPU buffer로 구현

적용 효과

  • 중복 K/V 계산 제거 → latency 대폭 감소
  • autoregressive decoding 시 per-token latency 최대 70~80% 절감
  • long-context input 처리 가능성 향상

3.6.4 기타 구조 특화 최적화

  • Flash Attention: memory-efficient attention 구현으로 연산 복잡도 및 memory usage 개선
  • Fused LayerNorm: variance + mean + normalization을 단일 kernel에서 처리
  • Residual Add Fusion: residual connection을 LayerNorm 또는 MatMul과 병합하여 memory copy 제거
  • Past/Present 시점 관리를 위한 decoder loop unrolling 등

3.6.5 Transformer-Specific Optimization 정리표

Optimization Technique 적용 대상 주요 효과
Fused Multi-Head Attention Q/K/V, Softmax, MatMul 등 latency 2~3배 개선, memory 이동 제거
Rotary Embedding Precompute Rotary pos embedding sin/cos 제거, token 생성 속도 개선
Key/Value Caching decoder attention 중복 연산 제거, token latency 절감
Flash Attention / Fused LN attention, LayerNorm block memory usage 절감, 연산 병합 최적화
  • 특히 attention block 전체를 plugin 또는 fused kernel로 대체하는 방식은
    실측 기준 가장 큰 성능 개선 효과를 가져올 수 있음

3.6.6 Transformer-Specific Optimization 예시

[예시 3.6.1] Fused Multi-Head Attention

문제  
- 일반적인 Multi-Head Attention 구조는 다음과 같이 다수의 연산으로 분해됨

  1. x → Linear(W_q) → q  
     x → Linear(W_k) → k  
     x → Linear(W_v) → v  
  2. q, k, v → Reshape / Transpose → (B, H, S, D_h)  
  3. attn_score = q × k^T / sqrt(d_k)  
  4. attn_prob = Softmax(attn_score)  
  5. output = attn_prob × v  
  6. output → Transpose / Reshape → Linear → Final output

- 각 연산이 별도 kernel로 호출되며, 중간 tensor를 GPU memory에 복사  
- 연산 간 memory 이동이 많고, kernel launch overhead가 누적됨  
- 특히 head 수(H)가 많거나, long sequence(S)일수록 성능 저하 심각

해결  
- Q/K/V projection을 하나의 weight로 통합 → FusedLinear  
- head split, matmul, softmax, dropout, weighted sum 연산까지 단일 kernel로 통합  
- TensorRT: `MultiHeadAttentionPlugin` 또는 `FusedMHA` op 사용  
- FlashAttention: CUDA 커널 기반 attention 연산 최적화  
- ONNXRuntime: `com.microsoft.FusedAttention` 커스텀 연산자 지원

적용 조건  
- 입력 shape이 고정되어 있을 것 (static B, S, H, D_h)  
- operation fusion 가능한 backend 사용할 것 (TensorRT, FlashAttention 등)

적용 효과  
- kernel 호출 수: 6~9개 → 1개  
- GPU memory 이동 제거  
- A100 기준, attention block latency 60~70% 절감 (batch=1, seq_len=128 기준)  
- decoder generation 속도 최대 2.5배 이상 향상
[예시 3.6.2] Rotary Embedding Precomputation

문제  
- RoPE(Rotary Positional Embedding)는 각 토큰의 position 정보를 다음과 같이 인코딩함:
  q_rotated = q * cos(pos_emb) + rotate(q) * sin(pos_emb)

- 여기서 pos_emb는 각 위치에 따라 고정된 sin/cos 값으로 구성됨  
- 일반 구현에서는 매 토큰마다 sin/cos 값을 직접 계산 → runtime에 반복되는 trigonometric 연산 발생  
- 특히 긴 시퀀스에서는 token마다 누적 연산이 많아져 rotary embedding block latency 증가  
- ONNX export 시에도 sin, cos 연산자 노드가 그래프에 고정됨 → 최적화 어려움

해결  
- sin/cos 값은 입력 내용과 무관하며, sequence length와 hidden size만 알면 사전 계산 가능  
- 따라서 RoPE에 사용될 sin_table, cos_table을 export 시점에 미리 계산하여 constant tensor로 삽입  
- runtime에는 매 토큰에 대해 sin(pos), cos(pos)를 계산하지 않고,  
  단순히 position index로 테이블에서 lookup만 수행

구현 방식  
1. sin_table: shape = (max_seq_len, dim // 2)  
2. cos_table: shape = (max_seq_len, dim // 2)  
3. PyTorch에서는 register_buffer로 저장하고, forward 시 indexing만 수행  
4. ONNX export 시 ConstantInitializer로 sin/cos table 삽입  
5. 기존의 trigonometric 연산자는 제거되며, Graph는 pure indexing 구조로 단순화됨

예시 구조 (ONNX 기준)

Before:  
  position → mul(theta) → sin → sin_tensor  
                        → cos → cos_tensor  
  q, k → rotation(q, sin_tensor, cos_tensor)

After:  
  position → Gather(sin_table), Gather(cos_table)  
  q, k → rotation(q, precomputed_sin, precomputed_cos)

적용 조건  
- 시퀀스 길이가 정해져 있거나 최대 길이가 고정 가능할 것  
- RoPE 구조가 standard할 것 (예: GPT-NeoX, LLaMA, Qwen 등)

적용 효과  
- sin/cos 연산 제거 → rotary embedding block latency 60~70% 감소  
- runtime 계산 제거 → token generation 속도 안정화  
- long-context 처리 시 positional embedding 연산 비용 거의 0에 수렴  
- ONNXRuntime, TensorRT 등에서 graph 간소화 + constant folding 최적화 자동 적용
[예시 3.6.3] Key/Value Caching (Decoder-only Transformer)

문제  
- GPT, LLaMA 등 decoder-only 모델은 다음 토큰을 생성할 때,  
  매 시점마다 전체 시퀀스를 다시 입력으로 넣고 forward pass 수행  
- 이때 모든 K/V를 매번 처음부터 계산하므로, 시퀀스 길이가 늘어날수록 연산량이 선형 증가함

예시 구조 (Without Caching)  
1. step 1: input = [t_1] → compute K_1, V_1  
2. step 2: input = [t_1, t_2] → compute K_1, K_2, V_1, V_2  
3. step 3: input = [t_1, t_2, t_3] → compute K_1 ~ K_3, V_1~ V_3  
→ 매 step마다 전체 context K/V를 반복 계산 → 비효율적

해결  
- 최초 step에서 계산한 K/V를 캐시에 저장  
- 이후 step에서는 query(q_t)만 계산하고, 이전 K/V는 cache에서 불러옴  
- attention 연산은 `q_t × [K_1~K_t]` 로 수행  
- 모델 구조는 그대로 유지하고, 내부적으로 buffer만 관리

예시 구조 (With Caching)  
1. step 1: input = [t_1]  
   → compute K_1, V_1 → 저장 (kv_cache[0] = K_1, V_1)  
2. step 2: input = [t_2]  
   → compute K_2, V_2 → kv_cache[1]  
   → attention(q_2, K_1~K_2, V_1~V_2)  
3. step 3: input = [t_3]  
   → compute q_3 only → attention(q_3, kv_cache[:3])

적용 방식  
- PyTorch에서는 past_key_values 형태로 캐시 전달  
- ONNX에서는 external buffer 연동 또는 custom decoder wrapper 사용  
- TensorRT는 `set_kv_cache()` API 또는 decoder plugin 내부에서 GPU buffer 직접 관리  
- FasterTransformer 등에서는 kv_cache를 CUDA memory에 pre-allocate한 후 매 step append

적용 조건  
- decoder-only 구조일 것 (autoregressive generation)  
- K/V 연산이 deterministic하고, attention mask가 재사용 가능할 것  
- 캐시 크기가 고정되거나 동적 관리 가능한 backend 사용할 것

적용 효과  
- 매 step마다 K/V 재계산 제거  
- token-by-token latency 70~85% 절감 (batch=1 기준)  
- long context generation 시 linear scaling 제거  
- GPT-style 모델의 real-time 응답성 향상 (e.g., 400ms → 70ms 수준)

주의사항  
- past K/V와 현재 입력의 position alignment가 정확해야 함  
- rotary embedding, absolute positional embedding 등 pos encoding이 캐시와 호환되어야 함  
- GPU memory 내에서 kv_cache가 충분히 확보되어야 함: seq_len × hidden_dim × num_layers
[예시 3.6.4] Flash Attention 및 기타 구조 특화 최적화

문제  
- 기존 Multi-Head Attention 구현은 다음과 같은 단계로 동작함:

  1. q, k → matmul → attn_scores  
  2. attn_scores → softmax  
  3. softmax(attn_scores) × v → output

- 각 단계에서 q×k^T, softmax, v와의 곱이 모두 별도 kernel로 실행됨  
- intermediate tensor가 메모리에 저장되고, softmax 출력도 full precision으로 유지됨  
- 긴 시퀀스에서는 attention score가 커서 softmax overflow/underflow 가능성 존재  
- 연산량은 O(n²), memory usage도 O(n²)로 증가

해결  
- FlashAttention은 위 세 단계를 하나의 kernel로 통합함  
- Softmax를 계산과정 중 streaming 방식으로 누적 처리 → 중간 tensor 저장 생략  
- 메모리 상에 전체 attention matrix를 올리지 않고, block 단위로 나눠서 처리  
- CUDA kernel 수준에서 q, k, v의 access pattern을 최적화하고, fused 연산 수행

구현 방식  
- PyTorch: `flash_attn` 패키지 (CUDA kernel 기반), `xformers`도 유사 구조 제공  
- Huggingface Transformers는 `attn_implementation="flash_attention_2"` 옵션 지원  
- TensorRT-LLM 및 FasterTransformer는 자체 fused attention kernel 탑재  
- 일부 backend는 FlashAttention이 아닌 `memory-efficient attention`이나 `block sparse attention`으로 유사 기능 구현

적용 조건  
- GPU에서 실행될 것 (FlashAttention은 CUDA 전용)  
- padding 없는 dense attention 구조일 것  
- head 수와 hidden dim이 kernel block 크기에 맞을 것

적용 효과  
- attention block memory usage 최대 50% 절감  
- attention kernel latency 2~4배 개선 (A100, seq_len=1024 기준)  
- 중간 softmax tensor 제거 → backward 시에도 memory 절감 가능  
- long-context 모델에서 generation 속도 및 확장성 향상

기타 특화 최적화  
- Fused LayerNorm:  
  - mean, variance, normalization 단계를 단일 kernel로 처리  
  - memory copy 제거, 연산 latency 20~30% 절감  
- Residual Add Fusion:  
  - residual connection을 LayerNorm, MatMul 등과 병합  
  - GPU memory bandwidth 절감 + kernel 호출 수 감소  
- Decoder Unrolling:  
  - 1-step decoder loop를 unroll하여 static graph로 고정  
  - export 후 loop 제거 → graph simplification + caching 연동 최적화

3.7 Runtime-Aware Optimization

  • 대부분의 graph-level 최적화는 export 시점 또는 compile 시점에 정적으로 결정됨
  • 그러나 실제 inference 환경에서는 입력 길이, batch size, GPU 아키텍처, 메모리 상황 등에 따라
    최적의 연산 경로가 달라지므로, runtime에 동적으로 적용되는 최적화 기법이 중요함
  • 다섯 가지 주요 Runtime 시점 최적화 기법: Kernel Auto-Tuning, Memory Layout Selection, Dynamic Shape Allocation, Execution Plan Caching, Fallabck / Precision Promotion

3.7.1 Kernel Auto-Tuning

기법 개요

  • 동일한 연산이라도 block size, tiling, unroll factor 등 kernel 설정에 따라 성능이 크게 달라짐
  • Auto-tuning은 여러 설정을 benchmark하고 가장 빠른 구성을 선택하여 실행하는 방식

구현 방식

  • TensorRT: builder 시 수백 개의 profile을 테스트하고 best kernel 선택
  • PyTorch Inductor: Triton 기반으로 다양한 launch config를 시험 실행 후 best path 선택

적용 조건

  • tuning 시간이 허용될 것 (초기화 시 latency 증가)
  • device 정보, tensor shape가 안정적일 것

적용 효과

  • 연산자별 최적 kernel 선택 → latency 1.2~2.0배 개선
  • tuning 결과를 cache로 저장해 다음 실행부터 즉시 적용 가능

3.7.2 Memory Layout Selection

기법 개요

  • tensor shape이 동일하더라도, memory layout(NCHW vs. NHWC, row-major vs. column-major)에 따라
    연산 효율과 cache hit rate가 크게 달라짐
  • runtime 시점에서 최적 layout을 선택하거나 변환하는 방식

구현 방식

  • TensorRT: relayout 없이 실행 가능한 format 우선 선택 (e.g., kLINEAR, kCHW32)
  • ONNXRuntime: preferred_layout 지정 → backend가 layout-aware kernel 선택

적용 조건

  • backend가 여러 layout을 지원할 것
  • relayout 비용 < layout 최적화 이득일 것

적용 효과

  • memory access locality 향상
  • 특정 kernel에서 layout 최적화로 연산 성능 최대 30~50% 개선

3.7.3 Dynamic Shape Specialization

기법 개요

  • 모델이 dynamic shape을 지원하는 경우, runtime에 자주 등장하는 shape에 대해
    별도 실행 계획(plan)을 생성하고 재사용하는 방식

구현 방식

  • TensorRT: optimization profile 정의 (e.g., min/opt/max shape)
  • ONNXRuntime: dynamic shape에서 shape-specialized plan을 내부적으로 cache
  • PyTorch 2.x Inductor: trace된 shape-specific graph를 저장하고 dispatch

적용 조건

  • 입력 shape이 수십~수백 가지로 제한될 것
  • backend가 plan caching을 지원할 것

적용 효과

  • dynamic shape에서 발생하는 runtime shape checking 제거
  • shape별 plan 재사용 → latency 안정화
  • dynamic input 대응성 + static path 최적화 동시 확보

3.7.4 Execution Plan Caching

기법 개요

  • 연산자의 scheduling, memory allocation, kernel launch sequence 등을
    runtime 중 최초 실행 시 생성하고 이후 동일 조건에서는 캐시를 재사용

구현 방식

  • TensorRT: serialized engine plan 생성 및 저장
  • ONNXRuntime: graph execution plan 캐시 (EP 단위, session 단위)
  • PyTorch 2.x: ahead-of-time compile된 kernel plan 저장

적용 조건

  • 입력 조건이 고정되거나 유사하게 반복될 것
  • 캐시 관리 정책이 효과적으로 구성될 것

적용 효과

  • graph compile time 제거 → startup latency 감소
  • runtime scheduling 불필요 → 실시간 추론 효율 향상

3.7.5 Fallback / Precision Promotion

기법 개요

  • FP16, INT8 등 저정밀 연산에서 NaN, overflow 등 문제가 발생하면
    자동으로 FP32 경로로 전환하거나 kernel을 교체하여 안정성을 확보하는 방식

구현 방식

  • PyTorch AMP: autocast 내부에서 precision fallback 처리
  • TensorRT: FP16 kernel 실패 시 FP32 kernel로 자동 대체
  • ONNXRuntime: dynamic quantization 실패 시 fallback to float 연산자

적용 조건

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

적용 효과

  • mixed precision 실행 안정성 확보
  • low-precision 성능 + high-precision 안정성 동시 확보
  • 개발자의 수작업 개입 없이 자동 precision control 가능

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 속도 향상
Fallback / Precision Promote runtime FP16, INT8 경로 안정성 확보, 자동 precision 조정

3.7.7 Runtime-Aware Optimization 예시

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

문제  
- 같은 연산(MatMul, LayerNorm 등)이라도 block size, tiling 방식, memory layout 등에 따라 성능이 크게 달라짐  
- 예를 들어, MatMul의 shape = (32, 1024) × (1024, 4096)인 경우,  
  block 크기를 (16×16), (32×32), (64×64) 등으로 나누는 방식에 따라 성능이 2배 이상 차이날 수 있음

해결  
- TensorRT는 builder 단계에서 수백 개의 kernel config를 자동 생성  
- 각 config에 대해 micro-benchmark를 실행하고 가장 빠른 연산 경로를 선택  
- 선택된 kernel은 serialized engine으로 저장되어 이후 실행 시 즉시 사용됨

구현 방식  
- builder config에서 `setMaxWorkspaceSize`, `setFlag(kENABLE_TACTIC_HEURISTIC)` 등을 설정  
- tuning time은 초기 10~60초 소요되며, 이후 재활용 가능  
- TVM, PyTorch Inductor, XLA 등도 유사한 auto-scheduler 또는 autotune 시스템 제공

적용 효과  
- 연산자별 최적의 kernel 경로 선택 → latency 최대 1.5~2.3배 개선  
- tuning 결과는 엔진과 함께 캐시되므로, 재사용 시 추가 비용 없음  
- GPU 아키텍처마다 최적 경로가 달라지는 경우 특히 효과적
[예시 3.7.2] Memory Layout Selection (ONNXRuntime + TensorRT)

문제  
- 동일한 tensor shape이라도 memory layout(NCHW vs. NHWC, row-major vs. column-major)에 따라  
  연산 성능이 크게 달라짐  
- 예:  
  - Tensor shape = (B, C, H, W)  
  - NCHW layout에서는 channel이 연속 → conv 연산 최적  
  - NHWC layout에서는 height/width가 연속 → broadcasting 연산 최적  
- layout이 연산에 최적화되어 있지 않으면 중간에 relayout 연산(insert Transpose) 발생  
  → memory copy + kernel fallback + latency 증가

해결  
- ONNXRuntime은 execution provider별 preferred layout을 선언하고,  
  해당 layout에 맞는 kernel을 자동 선택하거나, 필요 시 transpose 삽입 순서를 조정  
- TensorRT는 builder 최적화 중 `kLINEAR`, `kCHW32` 등 내부 tensor format을 자동 선택  
- MLIR 기반 backend는 layout-aware schedule을 통해, layout 변환 비용과 연산 이득을 trade-off 평가

구현 방식  
- ONNXRuntime: session option에서 layout-aware execution 활성화  
- TensorRT: builder flag에서 `setFlag(kPREFER_PRECISION_CONSISTENT_LAYOUT)` 지정  
- 일부 framework에서는 IR 단계에서 layout을 annotation하고 relay pass에서 layout 전파

적용 효과  
- layout mismatch로 인한 transpose 제거  
- memory locality 개선 → L2/L1 cache hit 증가  
- 연산 kernel이 vectorization-friendly layout을 사용할 수 있어 throughput 향상  
- A100 기준 Conv 연산에서 layout selection으로 latency 최대 40% 개선 사례 존재

주의사항  
- NCHW/NHWC는 주로 4D 이미지 tensor에서 사용되며, framework마다 기본 layout이 다름  
  - PyTorch: NCHW  (channel-first: Batch, Channel, Height, Width)
  - TensorFlow, TFLite: NHWC (channel-last: Batch, Height, Width, Channel)
- layout 불일치가 발생하면 transpose 연산이 삽입되며, 이는 연산량은 작지만 memory copy를 유발함  
- row-major/column-major는 2D 행렬에서의 저장 순서로,  
  - C, PyTorch, NumPy는 row-major  
  - Fortran, 일부 CuBLAS kernel은 column-major  
- backend가 지원하지 않는 layout을 강제로 사용하면 kernel fallback 또는 precision mismatch 발생 가능  
- 따라서 export 시점에 layout을 명시하거나, backend layout preference에 맞춰 사전 변환하는 것이 중요
[예시 3.7.3] Dynamic Shape Specialization (TensorRT Optimization Profile)

문제  
- Transformer 모델은 입력 길이(sequence length)나 batch size가 매 요청마다 달라질 수 있음  
- 이를 지원하려면 dynamic shape으로 export해야 하지만,  
  dynamic 연산은 backend가 최적화된 execution plan을 고정하지 못하므로  
  → runtime마다 shape 해석, memory plan 계산 등 오버헤드 발생  
  → latency와 throughput 모두 불안정해짐

예시 상황  
- 입력 shape이 (B, S, D)인데, S가 [16, 32, 64, 128, 256] 중 하나로 바뀌는 구조  
- ONNX나 TorchScript에서 dynamic_axes를 설정하면 각 inference 시점마다 graph 해석 필요  
- TensorRT는 모든 shape에 대해 단일 엔진을 사용하면 성능이 저하됨

해결  
- 자주 등장하는 입력 shape에 대해 미리 여러 개의 optimization profile 생성  
- 각 profile은 해당 shape에 최적화된 kernel config, memory layout, execution plan을 포함함  
- runtime에는 입력 shape을 분석해서 가장 적합한 profile로 dispatch

구현 방식  
- TensorRT:  
  - `builder.create_optimization_profile()`  
  - min/opt/max shape 지정 → engine에 복수 profile 포함  
  - runtime에 `context.set_optimization_profile()`로 선택 적용  
- ONNXRuntime:  
  - `ORT_ENABLE_PREPACK` 설정 시, shape-specific plan을 자동 캐시  
- PyTorch 2.x:  
  - TorchDynamo + Inductor가 shape guard를 생성해 shape-specific graph trace 수행

적용 효과  
- 자주 쓰이는 shape에 대해 static graph 수준의 성능 확보  
- dynamic shape이지만 latency, memory 사용량을 안정화  
- TensorRT 기준, 다양한 seq_len 입력에서 latency 편차 40~60% → 5~10%로 감소  
- Multi-turn inference에서 token 수가 바뀌는 상황에 효과적

주의사항  
- shape 개수가 너무 많으면 profile 수가 늘어나고 engine size가 커짐  
- unknown shape이 들어오면 fallback profile로 실행되며 성능 저하 발생  
- 따라서 workload 분석을 통해 대표 shape만 선택하는 것이 중요
[예시 3.7.4] Execution Plan Caching (ONNXRuntime SessionGraph)

문제  
- ONNX 모델을 처음 로딩할 때는 다음과 같은 과정이 발생함:
  1. 모델 graph 파싱
  2. 연산자 스케줄링
  3. tensor allocation 계획 수립
  4. backend EP(Execution Provider)별 kernel 할당

- 이 과정은 shape, device, EP config에 따라 달라지고,  
  초기 실행 시 latency가 100~500ms까지 늘어날 수 있음  
- 특히 서버에서 session을 매번 새로 여는 구조면 큰 병목 발생

해결  
- 초기 실행 시 생성된 execution plan (연산 순서, memory layout, kernel 선택 등)을  
  cache에 저장하고, 이후 동일 조건이면 그대로 재사용  
- session-level plan을 static하게 유지하거나, 다양한 plan을 자동 전환

구현 방식  
- ONNXRuntime:  
  - SessionOptions에서 execution plan caching 활성화 (`enable_mem_pattern`, `enable_cpu_mem_arena`)  
  - EP별 plan을 session 단위로 유지  
- TensorRT:  
  - serialized engine 자체가 execution plan임 (engine file)  
  - 입력 shape, precision, builder config가 같으면 plan 재사용 가능  
- PyTorch 2.x:  
  - TorchDynamo에서 traced graph를 AOT compile → plan 저장  
  - Inductor는 plan hash를 기반으로 동일 shape에서 재사용

적용 효과  
- 최초 실행 시 plan 생성 overhead 제거  
- 다음 호출부터는 ready-to-run → cold start latency 30~90% 감소  
- multi-session 또는 serverless 환경에서 효과적  
- ONNXRuntime 기준, 첫 추론 latency 220ms → 이후 호출 40~60ms로 단축

주의사항  
- session마다 다른 device나 EP 설정을 쓰면 plan 재사용 불가능  
- dynamic shape에서는 shape-specific plan을 여러 개 cache해야 함  
- plan cache가 커지면 메모리 footprint 증가 가능성 있음
[예시 3.7.5] Fallback / Precision Promotion (Runtime Stability)

문제  
- Mixed precision 또는 INT8 quantization 환경에서는 일부 연산이 낮은 precision에서 수치적으로 불안정해짐  
- 예:  
  - LayerNorm, Softmax, Exp, Div 등의 연산은 FP16/INT8에서 underflow 또는 overflow 발생  
  - 결과적으로 NaN, INF 또는 출력 정확도 저하 유발  
- backend에 따라 해당 연산에 대해 kernel이 아예 없거나, precision mismatch로 fallback이 발생

해결  
- 실행 시점에 연산의 입력, 정밀도, 안정성을 분석하여  
  특정 연산을 FP32 또는 더 높은 precision으로 자동 승격(promote)
- 또는 연산 자체를 다른 backend (e.g., CPU fallback, CUDA fallback)로 전환  
- 예: TensorRT는 FP16 빌드에서도 LayerNorm을 자동으로 FP32로 승격해 실행

구현 방식  
- TensorRT: builder 설정에서 `setFlag(kSTRICT_TYPES)`를 비활성화하면 precision fallback 허용  
- ONNXRuntime: execution provider가 지원하지 않는 연산은 자동으로 CPU fallback 수행  
- PyTorch AMP: autocast 컨텍스트 내에서 `custom_fp32_op` decorator로 연산 승격 지정 가능

적용 효과  
- 수치 안정성 확보 (특히 transformer 계열 모델에서)  
- NaN/INF 발생 방지  
- INT8, FP16 환경에서도 high-risk 연산에 대해 정확도 유지  
- 실제 inference에서는 전체 latency 증가 없이 안정성만 향상되는 경우 많음

주의사항  
- fallback이 많아지면 mixed precision 효과가 약해짐  
- fallback 연산이 메모리 layout이나 device 간 이동을 유발할 수 있음  
- profiling 시 어떤 연산에서 fallback이 발생하는지 추적하는 것이 중요

4. Computation Graph Optimization Frameworks

4.1 Overview of Optimization Frameworks

Computation graph optimization은 추론 속도와 자원 효율성을 개선하기 위한 핵심 기술이며,
이를 실현하는 다양한 프레임워크가 존재함.
각 프레임워크는 고유의 graph representation, 최적화 패스, 실행 backend를 바탕으로 최적화를 수행함.

최적화 프레임워크의 역할

  • 연산자 수준에서 fusion, folding, elimination 등을 수행
  • tensor shape, layout, precision을 정적으로 확정하거나 재구성
  • inference backend에 적합한 실행 계획 (execution plan) 생성
  • dynamic → static graph 변환 또는 runtime graph specialization 적용

대표적인 세 가지 프레임워크

  • TorchScript: PyTorch 모델을 graph IR로 변환하여 export 및 최적화 수행
  • ONNX + ONNX Runtime: 프레임워크 독립적 중간 표현으로 export 후 다양한 backend에서 실행
  • TensorRT: NVIDIA GPU에 특화된 고성능 추론 엔진, static graph를 기반으로 최적화 및 kernel scheduling 수행

적용 시점 기준 구분

구분 적용 시점 프레임워크
Export-time 모델 저장 시 TorchScript, ONNX export, Constant Folding
Build-time backend 빌드 시 TensorRT engine build, TVM compilation
Runtime 추론 시점 ONNXRuntime dynamic EP, TorchDynamo
Compile-time (AOT) 추론 전 JIT 컴파일 PyTorch 2.x (Inductor), XLA

프레임워크 선택은 다음 요소에 따라 결정됨

  • 모델의 graph 구조 (dynamic vs static)
  • 대상 환경 (GPU, CPU, edge device 등)
  • 최적화 요구 수준 (conservative vs aggressive)
  • ecosystem과의 통합성 (PyTorch-native, ONNX-compatible, CUDA-specific 등)

4.2 TorchScript

TorchScript는 PyTorch 모델을 static computation graph로 변환하여
Python 없이도 추론 가능한 형태로 만드는 중간 표현(intermediate representation)임.
Triton Inference Server에서 PyTorch 모델을 배포하기 위한 표준 방식으로 널리 사용됨.


4.2.1 Graph 구조 및 동작 방식

  • TorchScript는 PyTorch model을 ScriptModule 또는 TracedModule로 변환하여
    정적인 연산 그래프 형태로 표현함

  • 내부적으로 SSA(Static Single Assignment) 기반 IR을 구성하며,
    연산자, tensor 흐름, 조건 분기 등이 모두 graph node로 변환됨

  • 변환 방식은 두 가지:

    • Tracing (torch.jit.trace)

      • 예시 입력을 기반으로 forward 경로의 연산을 기록
      • Python control flow를 캡처하지 않음
    • Scripting (torch.jit.script)

      • 모델 내 control flow, 반복문, 조건문 등을 직접 파싱하여 변환
      • Python 문법 제약 있음 (e.g., 타입 명시 필요)
  • 변환된 모델은 .pt 형식으로 export 가능하며,
    Python 없이 C++ 기반 LibTorch 또는 Triton PyTorch backend에서 직접 실행 가능함


4.2.2 지원 최적화 기법

  • TorchScript는 static graph 상에서 기본적인 graph-level 최적화 패스를 수행함
    (주로 optimize_for_inference() 또는 export 시점에 자동 적용)

    • Operator Fusion

      • Linear → ReLU → LayerNorm 등 인접 연산자를 하나의 kernel로 병합
      • 메모리 이동 제거, launch overhead 감소
    • Constant Folding

      • 연산 결과가 고정된 constant일 경우, 계산을 미리 수행하여 graph에 삽입함
    • Dead Code Elimination

      • 추론 시 불필요한 분기, Dropout, backward 연산 등 제거
    • Control Flow Inlining

      • 반복문이나 조건문을 static graph 내부로 unroll하여 실행 흐름 단순화
    • Type Inference / Shape Propagation

      • tensor의 dtype, shape을 정적으로 추론하여 연산자 변환에 활용
  • 최적화 수준은 제한적이지만,
    Python runtime을 제거하고 추론용 경량 graph로 정리하는 데 유리함


4.2.3 장점 및 한계

장점

  • Python-free 환경에서 추론 가능 (Triton, C++, mobile 등)
  • PyTorch-native 모델을 별도 변환 없이 export 가능
  • 정적인 graph 표현을 통해 메모리 추론, 연산 병합, shape 고정 등에 유리
  • Triton Inference Server에서 .pt 형식으로 바로 배포 가능

한계

  • tracing 방식은 dynamic control flow를 정확히 반영하지 못함
  • scripting은 Python 문법 제약 존재 (함수 정의 방식, 타입 주석 등 필요)
  • memory layout, kernel selection, low-level precision 최적화는 미지원
  • export 오류 시 debugging이 복잡하고 예외 처리가 어려움
  • static quantization을 실질적으로 지원하지 않음

4.2.4 Triton Inference Server에서의 실전 활용

  • TorchScript는 Triton Inference Server의 PyTorch backend에서
    직접 로딩 및 추론이 가능한 유일한 정식 포맷임

  • 실전 적용 흐름

    1. 모델 학습 (PyTorch native)
    2. torch.jit.trace(model, example_input) 또는 torch.jit.script(model)로 graph 변환
    3. .pt 파일을 Triton의 model_repository/에 저장
    4. config.pbtxt에서 backend를 pytorch로 지정하고, input/output 정의
    5. Triton이 static graph를 로드하여 추론 서비스 수행
  • 추가 응용:

    • TorchScript + FP16 변환 → latency 절감
    • TorchScript + INT8 quantization (partial 지원)
    • TorchScript + ensemble 모델 구성 → preprocessing/postprocessing 분리 가능
  • 서비스 환경에서의 장점:

    • 별도 변환 없이 PyTorch 학습 모델을 즉시 배포 가능
    • GPU 환경에서 Python 제거 → 메모리 절감 및 안정성 향상
    • multi-model ensemble, model version 관리 등 Triton 기능과 통합 용이

4.2.5 TorchScript Code

TorchScript는 모델 구조를 static graph로 변환하여 .pt 파일로 저장 가능하며,
Triton Inference Server에서는 PyTorch backend를 통해 해당 .pt 파일을 직접 로딩하여 추론 수행함.

  1. 기본 export 절차
    PyTorch 모델을 eval() 모드로 전환한 후, 예시 입력과 함께 torch.jit.trace()를 호출하여
    TorchScript 형태로 변환하고 .pt 파일로 저장함.

    # 기본 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()
    model.eval()  # 반드시 eval() 모드에서 export
    example_input = torch.randn(1, 128)
    
    # tracing 방식으로 TorchScript 변환
    traced_model = torch.jit.trace(model, example_input)
    
    # 파일 저장
    traced_model.save("model.pt")
    

조건 분기나 반복문 등 dynamic control flow가 있을 경우에는 torch.jit.script() 사용을 권장함.

# scripting 기반 TorchScript export

import torch
import torch.nn as nn

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):
        # 입력에 따라 분기하는 control flow 포함
        if x.mean() > 0:
            x = self.linear(x)
        else:
            x = self.dropout(x)
        return x

# 모델 인스턴스 생성
model = MyScriptedModel()
model.eval()

# scripting 방식으로 TorchScript 변환
scripted_model = torch.jit.script(model)

# 저장
scripted_model.save("scripted_model.pt")

  1. Triton Inference Server 모델 구조 설정
    model_repository/ 디렉토리 내부에 모델 이름과 버전 폴더(1/)를 만들고,
    .pt 파일과 함께 config.pbtxt를 작성하여 모델 등록.

    Triton 모델 디렉토리 구조 및 config 설정
    
    # 디렉토리 구조 (예시)
    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]
      }
    ]
    

  1. 추론 최적화를 위한 추가 기법
  • Inference 전용 최적화 적용
    torch.jit.optimize_for_inference()를 통해 Dropout 제거, constant folding 등을 적용

    optimize_for_inference 적용 예시
    
    import torch
    import torch.nn as nn
    from torch.jit import optimize_for_inference
    
    class MyModel(nn.Module):
        def __init__(self):
            super().__init__()
            self.linear = nn.Linear(128, 64)
            self.dropout = nn.Dropout(0.1)
            self.relu = nn.ReLU()
    
        def forward(self, x):
            x = self.linear(x)
            x = self.dropout(x)  # 학습용 dropout
            return self.relu(x)
    
    # eval 모드 전환 후 TorchScript 변환
    model = MyModel().eval()
    example_input = torch.randn(1, 128)
    ts_model = torch.jit.trace(model, example_input)
    
    # inference 최적화 적용
    optimized_model = optimize_for_inference(ts_model)
    
    # 저장
    optimized_model.save("optimized_model.pt")
    
  • FP16 변환
    PyTorch AMP (torch.cuda.amp)를 이용해 half precision 변환 및 저장 가능.
    단, Triton에서 FP16 추론이 활성화되려면 config 파일에서 명시적으로 TYPE_FP16 지정 필요.

    AMP 기반 FP16 추론용 TorchScript export
    
    import torch
    import torch.nn as nn
    from torch.cuda.amp import autocast
    
    class MyModel(nn.Module):
        def __init__(self):
            super().__init__()
            self.linear = nn.Linear(128, 64)
            self.norm = nn.LayerNorm(64)
    
        def forward(self, x):
            return self.norm(self.linear(x))
    
    # 모델 준비
    model = MyModel().eval().cuda()
    example_input = torch.randn(1, 128).cuda()
    
    # AMP context 안에서 tracing 수행
    with autocast(dtype=torch.float16):
        traced_model = torch.jit.trace(model, example_input)
    
    # 저장 (model.pt는 여전히 FP32 weight 포함, 추론은 FP16에 적합)
    traced_model.save("model_fp16.pt")
    

4.3 ONNX

ONNX(Open Neural Network Exchange)는 PyTorch, TensorFlow, MXNet 등의 딥러닝 프레임워크로부터
모델을 export하여 공통 형식으로 교환/배포/최적화할 수 있도록 설계된 중간 표현(intermediate representation)임.

ONNX는 학습이 끝난 모델을 추론 최적화 중심으로 export하며,
ONNX Runtime, TensorRT, OpenVINO 등 다양한 backend에서 동일한 모델을 실행 가능하게 함.

Triton Inference Server는 ONNX를 공식 지원하며, GPU 및 CPU 모두에서 고성능 추론을 제공할 수 있음.


4.3.1 Graph 구조 및 동작 방식

  • ONNX는 computation graph를 static graph 형태로 export하며,
    연산자(op), tensor 흐름, shape 정보를 모두 명시적으로 포함함
  • export 시점에 모델 구조와 weight가 완전히 고정되며,
    Python-free 추론 환경에서 C++/CUDA 기반 실행이 가능함
  • PyTorch에서는 torch.onnx.export() 함수를 통해 ONNX 모델을 생성하며,
    내부적으로 tracing 방식으로 graph를 기록함
  • 생성된 .onnx 파일은 ONNX IR(GraphProto) 형식이며,
    ONNX Runtime, TensorRT, OpenVINO 등에서 직접 로드하여 실행 가능

4.3.2 지원 최적화 기법

ONNX는 static graph를 기반으로 하기 때문에
다양한 수준의 최적화 패스가 적용 가능함 (IR-level, kernel-level, backend-specific 등)

대표적인 최적화 기법:

  • Operator Fusion

    • Linear → Add → LayerNorm → GELU 등을 하나의 fused node로 대체
    • ONNX Runtime + ORT graph optimizer에서 자동 적용
  • Constant Folding

    • shape 계산, mask 생성, embedding 사전 계산 등을 static tensor로 전환
    • export 시점 또는 ONNX optimizer에서 수행
  • Dead Code Elimination

    • Dropout, backward 연산, train-only branch 제거
    • PyTorch에서 model.eval() 후 export 필요
  • Shape Inference

    • dynamic 연산 제거를 위한 사전 shape 계산
    • ONNX Runtime 및 external shape inference 도구로 정적 변환 가능
  • Quantization (INT8)

    • static quantization을 위한 calibration 적용 가능
    • ONNX Quantization Toolkit 또는 ORT-QDQ graph 패턴 활용
  • Layout 변환 / Memory Planning

    • NHWC ↔ NCHW 변환, weight layout 최적화
    • TensorRT EP에서 Tensor Core 최적화 layout 자동 적용

4.3.3 장점 및 한계

장점

  • 프레임워크 독립적 모델 배포 가능 (PyTorch, TensorFlow 등 모두 지원)
  • 다양한 backend (ONNX Runtime, TensorRT, OpenVINO 등)와 호환성 높음
  • INT8, FP16 등 다양한 precision 변환 지원

한계

  • tracing 기반이기 때문에 dynamic control flow 표현이 어렵거나 제한됨
  • export 오류 발생 시 debug 난이도 높음
  • PyTorch → ONNX로 변환 시 일부 연산자는 호환되지 않음 (aten::onnx:: mapping 문제)
  • weight sharing, custom kernel 등은 변환 시 누락될 수 있음

4.3.4 Triton Inference Server에서의 실전 활용

  • ONNX는 Triton에서 가장 일반적으로 사용되는 모델 포맷 중 하나임

  • Triton의 onnxruntime backend 또는 tensorrt backend를 통해
    ONNX 모델을 GPU 또는 CPU 환경에서 추론 가능

  • 실전 적용 흐름:

    1. PyTorch에서 torch.onnx.export().onnx 모델 생성
    2. model_repository/my_model/1/model.onnx에 저장
    3. config.pbtxt에서 backend 지정 (onnxruntime 또는 tensorrt)
    4. Triton이 static graph를 로딩하여 추론 서비스 제공
  • Backend 선택:

    • onnxruntime: 범용 CPU/GPU 지원, dynamic shape, INT8 지원
    • tensorrt: 고정 shape, FP16/INT8 가속 최적화, latency 중시
  • 추가 최적화 적용 방법:

    • onnxsim을 통해 redundant 노드 제거 및 shape 고정
    • onnxruntime_tools 또는 onnx-graphsurgeon을 활용한 수동 그래프 수정
    • TensorRT builder에서 FP16, INT8 엔진 생성 (calibration 필요)

4.3.5 ONNX Code

PyTorch에서 학습된 모델을 ONNX로 export할 때는 torch.onnx.export() 함수를 사용함.
대부분 tracing 기반으로 graph를 변환하며, .onnx 파일로 저장됨.

기본적인 흐름은 다음과 같음:

  1. 모델 정의 및 예시 입력 준비
  2. eval 모드 전환
  3. torch.onnx.export 호출
  4. 출력 경로 및 opset 지정
  5. optional: dynamic_axes, input_names, output_names 지정
  • 기본 ONNX export

    # 기본 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)
    
    # 기본 ONNX export
    torch.onnx.export(
        model,
        dummy_input,
        "model.onnx",
        opset_version=13,
        input_names=["input"],
        output_names=["output"]
    )
    
  • dynamic_axes 포함 export

    # dynamic_axes 포함 ONNX export
    
    # batch_size 및 sequence_length가 유동적인 경우, dynamic_axes 지정
    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"}
        }
    )
    
  • opset + custom export 옵션

    # custom export 옵션 (export_params=False, training mode)
    
    # 학습 중간 그래프를 추출하거나, weight를 별도 관리할 경우
    torch.onnx.export(
        model,
        dummy_input,
        "model_raw.onnx",
        export_params=False,      # weight 포함하지 않음
        do_constant_folding=False, # constant folding 생략
        training=torch.onnx.TrainingMode.TRAINING
    )
    
  • onnx-simplifier 적용 예시

    # onnx-simplifier 적용
    
    # 설치: pip install onnxsim
    import onnx
    from onnxsim import simplify
    
    # 원본 모델 로드
    model = onnx.load("model_dynamic.onnx")
    
    # simplify 적용 (constant folding, redundant node 제거 등)
    model_simp, check = simplify(model)
    
    # 검증 후 저장
    assert check
    onnx.save(model_simp, "model_simplified.onnx")
    
  • config.pbtxt와 Triton 연결 예시

    # Triton Inference Server config.pbtxt (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
      }
    ]
    

ONNX Static Quantization

  • Calibration Dataset 정의

    # Calibration 데이터셋 정의
    
    # Calibration용 dummy 데이터 생성기 정의
    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)
    
  • ONNX Static Quantization 수행

    [코드예시 4.3.5.7] ONNX Static Quantization 수행
    
    from onnxruntime.quantization import quantize_static, QuantType
    
    # 경로 설정
    fp32_model_path = "model_simplified.onnx"
    int8_model_path = "model_int8.onnx"
    
    # Calibration 데이터 리더 준비
    calib_reader = MyCalibDataReader()
    
    # Static Quantization 실행
    quantize_static(
        model_input=fp32_model_path,
        model_output=int8_model_path,
        calibration_data_reader=calib_reader,
        quant_format="QDQ",  # Quant-Dequant 노드로 표현
        activation_type=QuantType.QUInt8,
        weight_type=QuantType.QInt8,
        optimize_model=True
    )
    
  • Triton에 적용 가능한 INT8 ONNX 모델 저장

    # Triton에서 INT8 ONNX 모델 배포
    
    # config.pbtxt에서 precision mode 지정 (tensorrt backend일 경우)
    
    name: "my_model_int8"
    platform: "tensorrt_plan"
    max_batch_size: 16
    
    input [
      {
        name: "input"
        data_type: TYPE_FP32  # Triton이 자동 quantize
        dims: [128]
      }
    ]
    
    output [
      {
        name: "output"
        data_type: TYPE_FP32
        dims: [64]
      }
    ]
    
    instance_group [
      {
        kind: KIND_GPU
        count: 1
      }
    ]
    
    precision_mode: INT8
    

4.4 TensorRT

TensorRT는 NVIDIA에서 제공하는 고성능 딥러닝 추론 최적화 라이브러리임. FP16, INT8, layer fusion, kernel auto-tuning 등의 최적화를 통해 GPU 상에서 최소 latency, 최대 throughput을 달성하는 것이 목적임.

TensorRT는 PyTorch, ONNX, TensorFlow 모델을 입력으로 받아 CUDA-compatible static inference engine (.plan 파일)으로 변환하여 추론함.


4.4.1 구조 및 최적화 파이프라인

  • TensorRT는 입력 모델(graph)를 parsing → optimization → engine 생성의 3단계로 처리함
  1. Graph parsing

    • ONNX 모델(.onnx)을 로드하여 internal graph로 변환
    • Tensor, shape, op type, dependency 정보 추출
  2. Graph optimization

    • Operator fusion, constant folding, dead code elimination, layout transformation 적용
    • Precision별 (FP32/FP16/INT8) 연산자 선택
  3. Engine building

    • CUDA kernel을 기반으로 layer별 최적의 실행 계획 생성
    • shape, batch size, precision이 고정된 .plan 파일 생성

4.4.2 지원 최적화 기법

TensorRT는 다음과 같은 연산 최적화 패스를 자동 적용함:

  • Layer Fusion

    • Conv → ReLU → BatchNorm 등을 single kernel로 통합
    • Residual + LayerNorm, Q/K/V + MatMul 등 Transformer 구조도 통합 가능
  • Precision Tuning

    • FP32 → FP16 또는 INT8 변환
    • Softmax, LayerNorm 등은 자동으로 FP32 fallback 적용
  • Memory Planning

    • activation tensor reuse, buffer folding
    • GPU memory 최적 분배
  • Dynamic Tensor Shapes (Shape Optimization Profile)

    • 여러 input shape에 대해 최적 경로 미리 컴파일
    • 배치 크기, sequence length 별 profile 생성 가능
  • Kernel Auto-Tuning

    • 동일 연산에 대해 여러 kernel 후보 비교 후 latency 최저 kernel 선택

4.4.3 Engine 구성 및 저장

  • TensorRT에서 최종 생성되는 모델은 .plan 파일로 저장됨

    • 정적 그래프 구조 + 최적화된 CUDA kernel + memory binding 포함
    • 입력 shape, batch size, precision 등이 고정되어 있음
  • 엔진 구성 시 지정 가능한 파라미터:

    • max_batch_size: 최대 배치 크기
    • min_shape, opt_shape, max_shape: Dynamic shape profile 설정
    • precision_mode: FP16 / INT8 지정
    • workspace_size: GPU 메모리 사용 한도 (MB 단위)
  • Python, C++, CLI(TensorRT CLI builder) 등을 통해 engine 생성 가능


4.4.4 Triton Inference Server 연동

  • Triton에서 .plan 파일을 사용하려면 platform: "tensorrt_plan"으로 지정해야 함

  • Triton은 shape profile 및 precision 정보를 기반으로 engine을 자동 로딩

  • 실제 적용 흐름:

    1. ONNX 모델 준비 → optimize → TensorRT engine 생성 (model.plan)
    2. model_repository/my_model/1/model.plan 저장
    3. config.pbtxt에 input/output 정의, precision_mode, dynamic_shape 설정
    4. Triton 서버 시작 → engine 로딩 및 서비스
  • INT8 사용 시:

    • TensorRT INT8 builder에서 calibration cache 사용 가능
    • calib.cache 파일을 함께 제공하여 재학습 없이 deployment 가능

4.4.5 장점 및 한계

장점

  • GPU 최적화 수준이 가장 높음 (특히 FP16, INT8)
  • kernel auto-tuning, memory plan이 내장되어 있음
  • 다양한 input shape에 대해 profile 생성 → 유연성 확보
  • ONNX 외에도 PyTorch (Torch-TensorRT), TensorFlow, Keras 직접 지원

한계

  • 엔진 생성이 복잡하고 빌드 시간 오래 걸림
  • dynamic shape을 완전히 지원하지 않음 (profile에 명시된 shape만 사용 가능)
  • engine 재사용이 어려움 (환경 바뀌면 다시 빌드 필요)
  • proprietary binary → NVIDIA 환경 외 재현 어려움

4.4.6 TensorRT Code

  • ONNX → TensorRT engine으로 변환

    # 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)
    
    # ONNX 모델 로드
    with open(onnx_path, "rb") as f:
        assert parser.parse(f.read())
    
    # config 설정
    config = builder.create_builder_config()
    config.max_workspace_size = 1 << 30  # 1GB
    config.set_flag(trt.BuilderFlag.FP16)
    
    # dynamic shape profile 설정 (batch_size: 1~16)
    profile = builder.create_optimization_profile()
    profile.set_shape("input", (1, 128), (4, 128), (16, 128))  # min/opt/max
    config.add_optimization_profile(profile)
    
    # engine 생성
    engine = builder.build_engine(network, config)
    
    # 저장
    with open(engine_path, "wb") as f:
        f.write(engine.serialize())
    
  • profile 설정, precision 선택, calibration 적용

    # INT8 Calibration Cache 생성
    
    # ONNX → TensorRT 변환 시 calibration data로 INT8 engine 생성
    
    from calibrator import MyEntropyCalibrator  # 사용자 정의 calibrator
    config.set_flag(trt.BuilderFlag.INT8)
    config.int8_calibrator = MyEntropyCalibrator(cache_file="calib.cache")
    
    # engine 빌드 시 자동으로 cache 생성 후 저장됨
    
    # 사용자 정의 INT8 Calibrator (Entropy)
    
    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)
    
  • Triton에서 .plan 파일로 추론하는 config 예시

    # Triton config.pbtxt (TensorRT 엔진 사용)
    
    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"
      }
    }
    
  • trtexec를 활용한 TensorRT 엔진 생성

    # 기본 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
    

5. Latency Optimization Result in Enterprise-Scale System

Transformer 기반 임베딩 시스템에서 실시간 추론 속도 개선을 위해 하드웨어 교체, 정밀도 축소, 그래프 최적화를 적용함.
token 길이에 따른 latency 구성 요소를 분석하고, 각 최적화 단계가 실제 성능에 미치는 영향을 정량적으로 보고.


5.1 실험 설정

  • 대상 모델: BAAI/bge-m3
  • Task: 단일 문서 임베딩 추론
  • 입력 길이: 64 ~ 8192 tokens
  • 최대 입력 길이: 4096 token
  • 실행 환경: Triton Inference Server + (TensorRT, ONNX, TorchScript)
  • GPU: NVIDIA T4 (baseline) → NVIDIA L4
  • Precision: FP32 → FP16
  • Benchmark 방식: Per-document latency 측정 (batch=1)

5.2 최적화 단계 구성

최적화 단계 주요 구성
Current T4 + FP32 + Static Shape (= Fixed Input Length)
OpenAI Embedding API OpenAI Large-v3 API Response Latency
Optimized GPU L4 + FP32 + Dynamic Graph (= Variable Length)
Quantization 적용 L4 + FP16 + Dynamic Graph
Ours (최종 최적화) L4 + FP16 + Static Graph (Computation Opt)
  • 색상 구분:

    • 빨간색: L4 + FP32
    • 연두색: L4 + FP16
    • 하늘색: 최종 최적화 버전 (FP16 + Graph Optimization)

5.3 실험 결과


5.3 Token 길이별 분석 요약

Token 길이 주요 병목 요소 최적화 기법 반응성
64~256 Launch Overhead, I/O 최적화 효과 미미, 시스템 latency 비중 높음
512~1024 Memory Copy, Residual 연산 FP16 및 Graph 최적화 효과 시작
2048~8192 Activation Memory, Transpose Graph 최적화 효과 극대화
  • 4096 token 기준:

    • FP32: 약 1200ms
    • FP16: 약 550ms
    • Graph 최적화 포함: 120ms

5.4 적용된 최적화 기법 요약

  • FP16 기반 정밀도 축소:

    • 전체 연산 FP16 처리, LayerNorm 및 Softmax는 FP32 유지
    • PyTorch AMP 및 TensorRT builder 기반 precision pass 활용
  • Computation Graph 최적화:

    • Operator Fusion (Linear + Add + GELU + LayerNorm)
    • Memory Reuse (중간 tensor buffer 재활용)
    • Transpose Elimination (불필요한 layout 전환 제거)
    • Common Subexpression Elimination (mask, embedding 공유)
    • Dead Code Elimination (Dropout 등 제거)
    • Weight Sharing 및 Buffer Folding

5.5 OpenAI API와 비교

기준 (ms) 512 tokens 1024 tokens 2048 tokens 4096 tokens 8192 tokens
OpenAI (US) 400 400 400 400 400
OpenAI (Japan) 650 650 650 650 650
Ours 13.5 26.6 81 121 125
  • Runtime Query 기준: OpenAI 대비 30~50배 빠른 Latency
  • Async Context 기준**:** OpenAI 대비 3~5배 빠른 Latency
  • Baseline(비 최적화) 기준: Fixed Length여서 항상 max_token_length의 지연시간을 요구했으나, Variable Length를 지원하면서 적은 토큰수에서 Latency 수백배 이상 개선. Runtime Latency가 중요한 Query는 토큰 수가 적어서 향상폭이 매우 크다는 점이 핵심. 그래프 최적화를 통해서 Async Context Latency도 수십배 이상 개선.

5.6 실전 적용 시 고려사항

  • Precision 관련:

    • LayerNorm/Softmax는 FP16 rounding에 민감 → FP32 유지 필요
    • A100, L4 등 mixed precision 지원 GPU에서 FP16 효과 최대화
  • Memory 관리:

    • 2048 token 이상에서는 activation memory가 병목 → memory reuse 최적화 병행 필요
  • 배포 측면:

    • 4096~8192 token 추론 latency 150ms 이내 확보 → OpenAI 대체 가능한 Latency 도달

5.7 최적화 결과 요약

  • 단순한 하드웨어 교체(T4→L4)만으로는 충분한 latency 개선이 어려움
  • A100, H100 수준의 GPU는 Enterprise 수준에서 상업용을 목적으로 하기에는 가격경쟁력 면에서 떨어짐
  • FP16 정밀도 축소와 Computation Graph 최적화를 병행 적용하면 긴 문서에서도 실시간 응답 시간 확보 가능
  • 특히 1024 token 이상 구간에서 Graph-level 최적화의 latency 감소 효과가 뚜렷하게 관찰됨
  • 실험 결과, 4096 token 기준 기존 시스템 대비 16배 빠른 추론, OpenAI 대비 최대 5배 개선 효과를 확인함

6. 결론

RAG 시스템에서 요구되는 실시간 응답 성능은 Transformer 기반 모델의 추론 속도에 의해 결정됨. 특히 cross-encoder 기반 문서 reranking 단계는 문서 수에 따라 연산량이 선형 증가하며, per-query latency의 핵심 병목으로 작용함. 이러한 문제를 해결하기 위해 PyTorch 기반 모델을 static computation graph로 변환하고, 정적 최적화 기법을 체계적으로 적용하는 방식이 유효함.

연산 구조 최적화, 데이터 흐름 최적화, 메모리/자원 최적화, 정밀도 축소, Transformer 특화 최적화 등 총 7개의 카테고리에 걸쳐 다양한 최적화 기법이 존재함. 연산 구조 측면에서는 Linear + Add + LayerNorm 병합, Q/K/V projection 결합 등 operator fusion을 통해 kernel 호출 수를 줄일 수 있으며, 데이터 흐름 최적화에서는 Transpose pair 제거, Common Subexpression Elimination 등을 통해 redundant 연산을 제거할 수 있음. memory reuse와 in-place 연산을 통해 peak memory 사용량을 줄이고, FP16 정밀도 축소를 적용하되 LayerNorm 및 Softmax는 FP32로 유지하여 안정성을 확보할 수 있음. 또한 rotary embedding 사전 계산, fused multi-head attention 등 Transformer 특화 기법을 포함하여 구조 최적화도 적용할 수 있음.

Computation graph 최적화 기법을 종합하면, subgraph 단위의 패턴 감지와 folding, 반복 구조 병합, shape inference, control flow simplification 등을 기반으로 정적 추론 경로를 구성함. 특히 inference latency가 중요한 short-query 환경에서는 launch overhead 및 memory copy 최적화가 실질적인 효과를 보이며, long-context에서는 activation memory reuse 및 Transpose 제거가 성능 개선의 주요 요인으로 작용함. 각 최적화는 개별적 적용보다 조합 적용 시 누적 효과가 크며, static graph 기반 구조에서 최대 효과를 발휘함.

최적화 프레임워크 관점에서는 TorchScript, ONNX, TensorRT를 중심으로 모델 graph를 변환하고 최적화함. TorchScript는 PyTorch-native 모델을 JIT 컴파일하여 Python runtime 제거 및 실행 최적화를 가능하게 하며, ONNX는 framework-agnostic 구조를 바탕으로 다양한 backend에 대응 가능함. TensorRT는 NVIDIA GPU에 특화된 엔진으로, kernel fusion, memory scheduling, precision tuning 등을 수행하여 최종 runtime 성능을 극대화함. 특히 ONNX → TensorRT 경로는 enterprise-grade inference 시스템에서 정적 추론 구조와 hardware acceleration을 동시에 만족시킬 수 있는 실용적 접근 방식으로 작용함.

실험 결과, token 길이 4096 기준 기존 T4 기반 FP32 추론 대비 약 10배 이상의 latency 개선 효과를 보였으며, FP16 정밀도 축소와 graph-level 최적화를 병행 적용할 경우 latency는 1200ms에서 121ms로 단축됨. OpenAI Embedding API와 비교 시에도 최대 5배 이상의 응답속도 우위를 확보함. 단순한 GPU 업그레이드만으로는 실시간 요구를 충족하기 어려우며, static graph 기반 최적화 기법의 적용이 필수적임.

결론적으로, Transformer 모델의 추론 효율을 근본적으로 향상시키기 위해서는 연산 단위가 아닌 computation graph 단위에서의 최적화 접근이 요구되며, 각 프레임워크가 제공하는 static graph 변환 및 compiler-level 최적화 기능을 통합적으로 활용할 수 있어야 함. 이러한 최적화 전략은 RAG 시스템을 포함한 다양한 고부하 딥러닝 서비스의 응답 성능과 운영 효율성을 결정하는 핵심 기술로 작용함.