김정인 강사님 (jikim@imguru.co.kr)
강의 자료
https://colab.research.google.com/github/imguru-mooc/mfu-optimization/blob/main/MFU_1_day.ipynb MFU 정의, 필요성, 연산 이해
×), 덧셈(+), 나눗셈(/), 지수(exp) 등의 실수 연산이 몇 번 수행되었는가를 정량적으로 나타내는 지표| 항목 | GPU Utilization | MFU (Model FLOPs Utilization) |
|---|---|---|
| 정의 | GPU가 현재 얼마나 바쁘게 동작 중인지 (%) | GPU가 낼 수 있는 최대 연산 성능 대비 실제 모델이 활용한 비율 (%) |
| 측정 방법 | nvidia-smi 명령어 등으로 실시간 사용률 확인 | FLOPs, 실행 시간, GPU 이론 성능을 기반으로 계산 |
| 단위 | % | % |
| 의미 | GPU가 일하는 시간의 비율 | GPU가 “얼마나 효율적으로” 일했는가 |
| 주요 병목 요인 | 데이터 로딩, I/O, 동기화 지연 | 연산 최적화 부족, 배치 크기, 커널 효율 |
| 활용 목적 | 시스템 상태 확인 | 모델 최적화 및 효율 분석 |
| 용어 | 의미 | 세는 단위 |
|---|---|---|
| FLOP | Floating Point Operation – 부동소수점 연산 1회(곱, 덧셈 등) | 사칙연산·exp·log 등 1회씩 |
| FLOPs | 총 부동소수점 연산 수 | 전체 연산량 |
| FLOPS | Floating Point Operations per Second – 초당 처리 가능한 연산량 | 하드웨어 처리 속도 단위 |
| MAC | Multiply–Accumulate – “곱하고 더하기”를 한 묶음으로 처리 | a * x + b 1회 |
“1 MAC = 2 FLOPs”라는 관계는 Multiply–Accumulate(곱셈-누산) 연산을 어떻게 세느냐의 기준 차이에서 발생함
- 2가 어디서 온 걸까?
- 딥러닝이나 신호처리에서 자주 등장하는 연산 예는 곱하기 + 더하기 형태
- 예:
- 이 연산을 세는 방식이 다름!
- FLOPs 기준: 곱셈(1 FLOP) + 덧셈(1 FLOP) = 총 2 FLOPs
- MAC 기준: (곱셈+덧셈)을 하나의 패키지 연산으로 보므로 1 MAC
- 즉, → 이 “2”가 바로 곱하기 1회 + 더하기 1회에서 온 것
- 왜 이렇게 셀까?
- 현대 CPU · GPU 하드웨어는 FMA (Fused Multiply–Add) 명령어를 지원
- FMA는 두 연산을 한 번에 수행하며 오차 없이 결과를 누적함
- 즉, 하드웨어 레벨에서는
a × b + c를 1 사이클 내에 처리하므로 성능 측정(MAC)에서는 1회로 계산하지만, 수학적으로는 곱셈 + 덧셈 → 두 연산으로 표시(FLOPs)하는 것- 변환 관계
- CNN에서 50 M MAC이 계산됨 → 논문에서 FLOPs로 표현하면 약 100 M FLOPs
- 핵심 요약
- MAC: (곱+덧) 하나로 묶어서 1 연산
- FLOP: 각각을 별도 연산으로 센다
- 그래서 1 MAC = 2 FLOPs
- 발생 원인 → “Fused Multiply–Add(FMA)” 구조 때문
model.add(Conv2D(32, (3, 3), input_shape=(28, 28, 1)))x = x.permute(0, 3, 1, 2)| 특징 | Keras 입력 형상 | PyTorch 입력 형상 |
|---|---|---|
| 기본 데이터 순서 | (batch, height, width, channels) (NHWC) | (batch, channels, height, width) (NCHW) |
| 이유 | TensorFlow 최적화 및 CPU 친화 | GPU 최적화, 메모리 접근 효율 |
| 변환 필요성 | Keras → PyTorch 전환 시 permute 필요 | PyTorch → Keras 전환 시 transpose 필요 |
char: 1byte => 8bit
int: 4byte => 32bit
↑ 정수
------
↓ 실수
float
double
char c=200;
printf("%d\n", c); // -56
char c = 0xff; // 11111111 → -1
short s = 0xfffc; // 1111 1111 1111 1100 → -4
int i = 0xfffffffe; // 1111 … 1111 1111 1110 → -2
typedef struct
{
char ch:2;
}BIT;
BIT b;
b.ch = 3; // -1
if(b.ch == 3) // 여긴 절대 실행 안 됨
…
typedef struct
{
char ch:1;
}BIT;
BIT b;
b.ch = 1; // -1
if(b.ch == 1)
…
char: 1byte => 8bit
int: 4byte => 32bit
↑ 정수
------
↓ 실수
float: 4byte => 32bit
double: 8byte => 64bit
float f = 10.25f;
→ 1010.01(2)

1000000000 →
1010.01 →

0.00000000001 →

'Mantissa (fraction)'(가수부)는 숫자의 유효 숫자 부분이며, 특히 부동소수점(floating-point) 표기법에서 소수점의 위치를 나타내는 'exponent'(지수부)와 함께 사용됩니다. 컴퓨터는 부동소수점수를 가수부와 지수부로 나누어 저장하며, 가수부는 숫자의 정밀도를 나타냅니다.
double: 8byte => 64bit
double f = 10.25;
float f = 0.1f;
0.0001100110011… →
float f = 0.1f;
float sum = 0.0f;
int i;
for (i=0; i<10000; i++)
sum += f;
printf("%f\n", sum);
double f = 0.1;
double sum = 0.0;
for (int i = 0; i < 10000; i++)
sum += f;
printf("%lf\n", sum);
부동소수점 오차에 의해 정확한 1000 출력이 보장되지 않음!

import numpy as np
a = np.array([1,2,3,4])
a.ndim // 1
a.shape // (4,)
len(a) // 4
a.shape[0] // 4
a[0] // 1 → index는 차원을 줄이는 연산
a[0:1] // [1] → slicing은 값을 줄임
a = np.array([[1,2],[3,4]])
a.ndim // 2
a.shape // (2,2)
len(a) // 2
a.shape[0] // 2
a[0] // [1,2] → index는 차원을 줄이는 연산
a[0:1] // [[1,2]] → slicing은 값을 줄임
a = np.array([[[1,2],[3,4]],[[5,6],[7,8]]])
a.ndim // 3
a.shape // (2,2,2)
len(a) // 2
a.shape[0] // 2
a[0] // [[1,2],[3,4]] → index는 차원을 줄이는 연산
a[0:1] // [[[1,2],[3,4]]] → slicing은 값을 줄임
a = np.arange(16).reshape(1,1,4,4)
a.ndim // 4
a.shape // (1,1,4,4)
len(a) // 1
a.shape[0] // 1
a[0] // [[1,2],[3,4]] → index는 차원을 줄이는 연산
a[0:1] // [[[1,2],[3,4]]] → slicing은 값을 줄임
a = np.array([1,2,3,4])
a // [1,2,3,4]
a.T // [1,2,3,4] → 1차원 배열은 전치 X
a.reshape(-1,1) // reshape을 통해 전치처럼 보이게 할 수 있음
a = np.array([[1,2],[3,4],[5,6]])
a → (3,2)
a.T → (2,3): [[1,3,5],[2,4,6]]
a = np.array([[[1,2],[3,4]],[[5,6],[7,8]]])
a.transpose(2,1,0) // [[[1,5],[3,7]],[[2,6],[4,8]]]`
a.transpose(2,0,1) // [[[1,3],[5,7]],[[2,4],[6,8]]]`
(1,32,32,3) → (1,3,32,32) a = np.array([1,2,3])
b = np.array([1,1,1])
a*b
a = np.array([1,2,3])
b = np.array([1,1,1])
np.sum(a*b)
a = np.array([1,2,3])
b = np.array([1,1,1])
np.dot(a,b)
배열에 인접한 개수가 같으면(공리에 맞으면) 행렬의 곱 가능: "첫 번째 행렬의 열 개수와 두 번째 행렬의 행 개수가 같아야 합니다"
(3,)(3,) → ()
a = np.array([1,2,3])
b = np.array([[1,1],[2,2],[3,3]])
np.dot(a,b) // [14,14]
(3,)(3,2) → (2,)
a = np.array([[1,1],[2,2],[3,3]])
b = np.array([1,2,3])
np.dot(a,b) // [3,6,9]
(3,2)(2,) → (3,)
a = np.array([[1,1],[2,2],[3,3]])
b = np.array([[1],[2]])
np.dot(a,b) // [[3],[6],[9]]
(3,2)(2,1) → (3,1)
a = np.array([[1,1],[2,2],[3,3]])
b = np.array([[1,1],[2,2]])
np.dot(a,b) // [[3,3],[6,6],[9,9]]
(3,2)(2,2) → (3,2)
x = np.array([[1,1],[2,2],[3,3]])
w = np.array([[1,1],[2,2]])
np.dot(a,b) // [[3,3],[6,6],[9,9]]
(3,2)(2,2) → (3,2)
(입력 데이터 수,특성 수)(특성 수, 뉴런 수)
x =np.array(
N = len(x) = 6
F = len(x) = 3
output = N - F + 1
output = 6-3+1
x =np.array(
N = len(x) = 6
F = len(x) = 3
output = N +2P - F + 1
output = 6+2-3+1
N = len(x) = 3
F = len(x) = 2
output = N + 2P -F +1
output = (3 + 2*0.5 -2) + 1
1 2 3
4 5 6
7 8 9
1 1
1 1
12 16
24 28
N = len(x) = 3
F = len(x) = 2
output = N + 20 -F +1
output = (3 + 2*0.5 -2) + 1
f(x) = max(0,x)
i = -1;, 11111111 → 255
forward
[1, -2, 3, -4, 5] → [0, 1, 0, 1, 0]미분
dout[mask]=0[3,0,-5,0,-7] ← [3,4,-5,6,-7](N*OH*O11W, C*FH*FW)(1*3*3, 1*2*2)col col_W
dw = col.T∙dout
np.repeat(b,3,axis=0)np.sum(b,axis=0)flops = 2 * H_out * W_out * K_h * K_w * C_in * C_out 에서 2를 곱하는 이유는, CNN의 부동소수점 연산에서 곱셈과 덧셈이 한 세트로 이루어지기 때문입니다.
각 출력 요소를 계산하기 위해,
따라서 각 출력 원소당 1회의 곱셈 + 1회의 덧셈 = 2회 연산(FLOPs) 이 발생합니다.
이걸 식으로 정리하면,
각 연산이 출력 원소마다 위 식에 곱해지고, 곱셈-덧셈 쌍이 2회이므로 최종적으로 2 * ... 곱셈이 붙는 것입니다.
즉, 2는 매번 곱셈과 덧셈(MAC: Multiply-Accumulate)이 둘 다 일어나면서 추가되는 부동소수점 연산 수를 반영한 계수입니다.
요약하면:
flops에서 2배는 부동소수점 곱셈과 덧셈을 각각 1회씩 수행하는 것을 합친 것 이 점을 이해하면 CNN 연산량 계산 시 2 * ...가 심플한 표준 공식을 만든 이유임을 알 수 있습니다.