AI 다이어트 실패 사건 · 연산자별 지원과 대체 · 이론
모든 연산자가 int8 로 가지는 않는다
한 줄 요약
양자화 도구는 자기가 아는 연산자만 int8 모양으로 바꾼다. 나머지는 float 로 남고, 그 경계마다 Q/DQ 왕복이 생긴다.
왜 이게 문제가 되나
quantize_static 을 돌리고 파일 크기가 줄었으면 일이 끝난 것 같다. 그런데 그래프를 열어 보면 절반은 예전 그대로다. 양자화 도구에는 어떤 연산자를 어떻게 다룰지 적어 둔 표가 있고, 그 표에 없는 연산자는 건드리지 않는다. 손대지 않은 연산자는 float 로 계산해야 하므로, 앞에서 int8 로 만든 값을 다시 float 로 풀고, 뒤에서 다시 int8 로 담는다.
그래서 "양자화했다" 와 "int8 로 돈다" 는 다른 말이다. 둘을 같다고 믿으면 기대한 만큼 빨라지지도 작아지지도 않는데 이유를 못 찾는다. 이유는 그래프 안에 다 적혀 있다 — 세어 보면 된다.
QDQ 판을 읽는 법
정적 양자화의 QDQ 형식은 연산자를 바꾸지 않는다. 대신 텐서마다 QuantizeLinear 와 DequantizeLinear 를 끼워 넣는다. 그래프에는 여전히 MatMul 이 float 로 적혀 있고, 실행기가 DQ -> 연산자 -> Q 묶음을 알아보고 int8 커널로 바꿔 돌린다.
그러니 그래프만 보고 "무엇이 int8 로 갈 수 있는가" 를 판정하려면 규칙이 필요하다. 이 실습은 이렇게 정한다. 모든 입력이 DequantizeLinear 에서 오고 모든 출력이 QuantizeLinear 로만 가는 연산자를 int8 로 감싸인 것으로 본다. 하나라도 어긋나면 그 연산자는 float 로 남는다.
감싸인 모양 남은 모양 DQ -> MatMul -> Q DQ -> MatMul -> Add -> Relu -> Q (int8 커널로 접힌다) (Add·Relu 는 float 섬)float 로 남은 연산자들이 서로 이어져 있으면 한 덩어리다. 이것을 float 섬이라 부르자. 섬은 개수도 중요하지만 경계 수가 더 중요하다. 섬 하나마다 들어오는 DequantizeLinear 와 나가는 QuantizeLinear 가 붙는다. 섬이 늘어날수록 경계가 늘고, 경계마다 값을 옮겨 담는 일과 반올림이 한 번씩 더 생긴다.
실측으로 한 가지가 더 눈에 띈다. 범위를 좁히지 않고 양자화하면 그래프에서 Relu 가 사라진다. 놀랄 일이 아니라 경계 하나로 접힌 것이다. 뒤따르는 QuantizeLinear 의 영점(zero_point)을 int8 의 아래끝인 -128 로 잡으면 담을 수 있는 값의 범위가 정확히 0 부터 시작하고, 음수는 담기는 순간 0 으로 잘린다. Relu 가 하던 일을 경계가 대신 하는 것이다. 이 실습 모델에서 재어 보면 은닉층의 영점이 실제로 -128 이고 표현 범위가 0.0 부터 시작한다. 세어 보지 않으면 census 에서 Relu 가 0 인 이유를 설명할 수 없다.
한 가지 더. 동적 양자화는 아예 다른 모양이다. MatMul 이 MatMulInteger 로 바뀌고 DynamicQuantizeLinear 가 붙는다. QDQ 판을 재던 잣대를 여기에 그대로 대면 엉뚱한 답이 나온다. 잣대는 형식마다 다시 정해야 한다.
현장에서 하는 일 세 가지
첫째, 범위를 좁힌다. 어떤 층에서 정확도가 무너진다면 그 노드만 빼면 된다. 도구는 nodes_to_exclude 로 노드 이름을, op_types_to_quantize 로 연산자 종류를 받는다. 이름으로 빼면 그 노드의 가중치가 float 로 남고, 종류로 좁히면 그 종류 밖의 연산자가 통째로 float 섬이 된다. 어느 쪽이든 무엇이 바뀌었는지 세어서 확인해야 한다 — 옵션을 주었다고 그대로 되었다고 믿지 않는다.
둘째, 모델을 고쳐 양자화되는 모양으로 만든다. MatMul 다음에 Add 가 오는 모양은 아주 흔한데, 이 둘을 Gemm 하나로 합치면 사이에 있던 중간 텐서가 사라진다. 중간 텐서가 사라지면 그 텐서에 붙어 있던 Q/DQ 한 쌍도 사라진다. 값은 그대로인데 경계만 줄어드는 것이다. 실측으로 3층짜리 모델에서 QuantizeLinear 가 7개에서 4개로 줄었다.
범위를 좁힐 때 흔히 놓치는 것이 하나 있다. 좁히는 것은 공짜가 아니다. 어떤 노드를 빼면 그 노드는 float 로 돌지만, 그 앞뒤에 있던 Q/DQ 는 대체로 그대로 남는다. 즉 int8 의 이득은 잃고 경계 비용은 치르는 자리가 생긴다. 그래서 "정확도가 흔들리니 일단 빼자" 는 판단도 세어 보고 해야 한다. 뺀 뒤의 섬 개수와 경계 수를 안 좁힌 판과 나란히 놓으면, 무엇을 잃고 무엇을 얻었는지가 숫자로 남는다.
셋째, 세어서 보고한다. 연산자 census, Q/DQ 개수와 왕복 수, 섬의 개수와 크기, 경계 수. 이 다섯 숫자면 "왜 기대만큼 안 줄었나" 라는 질문에 그래프로 답할 수 있다. 시간을 재기 전에 그래프를 먼저 읽는 편이 훨씬 빠르다.
실무에서 진짜 중요한 것
- 양자화 뒤에는 그래프를 열어 센다. 파일 크기만 보지 않는다.
- 판정 규칙을 글로 적어 둔다. 무엇을 int8 로 갔다고 볼 것인지는 정하는 것이지 주어지는 것이 아니다.
- 범위를 좁히는 옵션은 준 다음 차이를 세어 확인한다.
- 형식이 다르면 잣대도 다르다. 동적 판과 QDQ 판을 같은 자로 재지 않는다.
- 모델을 고칠 수 있으면 고친다. 옵션보다 그쪽이 효과가 큰 경우가 많다.
다음 실습에서 할 것
같은 모델을 다섯 가지로 내보낸 뒤, 그래프를 세는 분석기를 한 단계씩 키운다. 연산자 census, Q/DQ 왕복, float 섬, 섬마다의 경계 수를 차례로 세고, 범위를 좁힌 두 판과 원래 판의 차이를 낸다. 마지막에는 MatMul 과 Add 를 Gemm 으로 합쳐 다시 양자화하고 Q/DQ 가 몇 개나 사라졌는지 센다. 채점기는 매번 다른 층 수와 다른 제외 노드로 자기 모델을 만들어 여러분의 분석기를 실제로 돌리고 같은 수를 세어 대조한다.