- Published on
TensorFlow Lite INT8 양자화 실패 대표 에러 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
TensorFlow Lite INT8 양자화는 모바일·엣지에서 지연시간과 메모리를 크게 줄여주지만, 변환 파이프라인이 길고 제약이 많아 실패 케이스도 다양합니다. 특히 converter.optimizations 만 켠다고 끝나는 것이 아니라,
- 대표 데이터셋(representative dataset)로 캘리브레이션이 제대로 되는지
- 모델 그래프에 INT8로 내릴 수 없는 연산이 섞여 있는지
- 입력/출력 타입을 INT8로 강제할지, float로 남길지
같은 조건들이 맞물리면서 변환 실패 또는 런타임 에러가 발생합니다.
이 글에서는 현업에서 가장 자주 겪는 INT8 양자화 실패 패턴 7가지를 에러 메시지(또는 증상) 기준으로 묶고, 재현 코드와 함께 해결책을 제공합니다. 원인 분석 방식은 장애 대응 글에서 쓰는 체크리스트 접근을 따릅니다. 비슷한 스타일의 점검 글로는 K8s CrashLoopBackOff - readinessProbe 실패 7원인도 참고하면 좋습니다.
기본: 올바른 INT8 양자화 템플릿
아래 템플릿을 기준으로, 각 에러의 원인이 어디에서 발생하는지 역추적하면 빠릅니다.
import tensorflow as tf
import numpy as np
# 1) SavedModel 또는 Keras 모델 로드
model = tf.keras.models.load_model("./saved_model_dir")
# 2) Representative dataset
def representative_dataset():
# 실제 입력 분포를 반영해야 함 (중요)
for _ in range(100):
x = np.random.rand(1, 224, 224, 3).astype(np.float32)
yield [x]
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
# 3) INT8 강제(완전 정수 양자화)
converter.target_spec.supported_ops = [
tf.lite.OpsSet.TFLITE_BUILTINS_INT8
]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
# 4) 변환
try:
tflite_model = converter.convert()
open("model_int8.tflite", "wb").write(tflite_model)
except Exception as e:
print("Convert failed:", e)
이제부터는 위 흐름에서 어디가 깨지는지에 따라 7가지 대표 에러로 분류해 설명합니다.
1) 대표 데이터셋 미지정 또는 잘못된 yield 형식
흔한 증상/에러
ValueError: representative_dataset is required for full integer quantization- 또는 변환은 되는데 정확도가 급락(캘리브레이션이 사실상 실패)
TypeError류로yield값의 타입/구조가 맞지 않다는 메시지
원인
완전 INT8(입력/출력까지 int8)로 내리려면 캘리브레이션이 필수입니다. 이때 representative_dataset 는
- 모델 입력 개수와 동일한 리스트/튜플 형태로
yield해야 하고 - dtype은 보통 원본 입력 dtype(대부분 float32)
- shape은 실제 추론 shape과 호환
이어야 합니다.
해결
- 입력이 1개면
yield [x]형태를 지키기 - 입력이 여러 개면
yield [x1, x2]처럼 순서 일치 - 실제 서비스 입력 분포를 반영(랜덤 데이터는 최후의 수단)
def representative_dataset_from_npz(npz_path: str):
data = np.load(npz_path)
xs = data["inputs"] # 예: (N, 224, 224, 3)
for i in range(min(len(xs), 200)):
x = xs[i:i+1].astype(np.float32)
yield [x]
정확도 급락 문제는 대부분 여기서 시작합니다. 대표 데이터셋 품질은 단순 변환 성공/실패를 넘어 성능의 핵심입니다.
2) TFLITE_BUILTINS_INT8 강제 시 연산자 미지원
흔한 증상/에러
ConverterError: ... requires Flex opsSome ops are not supported by the native TFLite runtimeERROR: Op ... is neither a custom op nor a flex op
원인
모델 그래프에 INT8로 양자화 가능한 TFLite builtin 연산만 있어야 TFLITE_BUILTINS_INT8 로 완전 정수 변환이 됩니다. 하지만 다음이 섞이면 자주 깨집니다.
- 특정 activation, resize, normalization, fancy indexing
- TF 전용 연산(그래프에 남은
tf.*op) - 학습 그래프가 완전히 inference 그래프로 정리되지 않은 경우
해결
선택지는 3가지입니다.
- 모델을 수정해서 지원 연산만 사용
- 일부만 float로 남기는 하이브리드 양자화 허용
- Flex delegate(TF Select ops) 사용(단, 바이너리 커지고 모바일에서 불리)
하이브리드로 우회하는 예시는 다음과 같습니다.
converter.target_spec.supported_ops = [
tf.lite.OpsSet.TFLITE_BUILTINS_INT8,
tf.lite.OpsSet.TFLITE_BUILTINS, # float fallback 허용
]
# 입력/출력을 int8로 강제하지 않으면 성공률이 올라감
# converter.inference_input_type = tf.int8
# converter.inference_output_type = tf.int8
완전 INT8이 목표라면, 실패한 op 이름을 기준으로 그래프를 단순화하거나 대체 레이어로 바꾸는 것이 정공법입니다.
3) 입력/출력 dtype 강제(inference_input_type=int8)로 인한 불일치
흔한 증상/에러
- 변환은 되지만 런타임에서 입력 텐서 타입 불일치
Cannot set tensor: Got value of type FLOAT32 but expected type INT8- 또는 전처리/후처리에서 스케일 적용이 누락되어 예측이 망가짐
원인
converter.inference_input_type = tf.int8 는 “모델 입출력 인터페이스 자체를 int8로 바꾸겠다”는 의미입니다.
- 앱/서버에서 float 입력을 그대로 넣으면 즉시 타입 에러
- int8로 넣더라도
scale과zero_point를 적용한 양자화 입력이 되어야 함
해결
- 초기에 디버깅 목적이면 입력/출력은 float로 두고 내부만 양자화(성공률 높음)
- 완전 INT8을 쓰려면 런타임에서 quantization 파라미터를 읽어 변환
아래는 TFLite Interpreter에서 입력 스케일을 적용하는 예시입니다.
import numpy as np
import tensorflow as tf
interpreter = tf.lite.Interpreter(model_path="model_int8.tflite")
interpreter.allocate_tensors()
in_detail = interpreter.get_input_details()[0]
scale, zero_point = in_detail["quantization"]
x_float = np.random.rand(1, 224, 224, 3).astype(np.float32)
# float32 -> int8 quantize
x_q = (x_float / scale + zero_point).round().astype(np.int8)
interpreter.set_tensor(in_detail["index"], x_q)
interpreter.invoke()
현장에서 “변환은 성공했는데 결과가 이상하다”의 상당수가 이 스케일/제로포인트 적용 누락입니다.
4) 대표 데이터셋 shape가 동적 입력과 충돌
흔한 증상/에러
RuntimeError: Attempting to resize dimension ...ValueError: Cannot set tensor: Dimension mismatch- 변환 중
Calibrator단계에서 shape 관련 예외
원인
모델 입력이 동적 shape(예: 배치 가변, 시퀀스 길이 가변)인 경우, representative dataset이 제공하는 shape가
- 실제 입력 시나리오와 다르거나
- 캘리브레이션 중 특정 op가 고정 shape를 요구
하면 실패합니다.
해결
- 캘리브레이션에서는 가장 대표적인 고정 shape로 통일
- 가능하면 변환 전에 입력 shape를 고정한 모델로 export
- NLP 계열은 max length를 고정하고 pad/truncate를 대표 데이터셋에서도 동일하게 적용
MAX_LEN = 128
def rep_ds_text():
for _ in range(200):
token_ids = np.random.randint(0, 30000, size=(1, MAX_LEN), dtype=np.int32)
attn_mask = np.ones((1, MAX_LEN), dtype=np.int32)
# 입력이 2개인 모델 예시
yield [token_ids, attn_mask]
동적 입력은 양자화 자체보다 “캘리브레이션이 어떤 shape로 그래프를 통과하느냐”가 더 민감합니다.
5) per-channel 양자화 이슈로 특정 레이어에서 실패
흔한 증상/에러
- 변환 중
per-channel quantization관련 메시지와 함께 실패 - 특정 Conv/Dense에서 스케일 계산 문제
- 환경에 따라 같은 모델이 되고/안 되고가 갈림(버전 영향)
원인
INT8에서 가중치는 per-channel(채널별 스케일) 양자화를 쓰는 경우가 많습니다. 하지만
- 일부 연산 조합
- 특정 delegate/백엔드
- TF/TFLite 버전
에 따라 per-channel 지원이 제한되거나 버그가 있을 수 있습니다.
해결
- TensorFlow 버전을 올리거나(또는 내리거나) 재현 확인
- 문제가 되는 레이어를 분리/대체(예: DepthwiseConv 구성 변경)
- 우회로로 float fallback을 잠깐 허용해 원인 op를 특정
버전 차이로 인한 비결정적 실패는, 캐시/환경 문제를 다루는 글에서처럼 “재현 가능한 최소 케이스”를 만드는 게 중요합니다. 운영에서 원인 추적이 어려울 때는 Next.js 14 RSC 캐시로 데이터가 안 바뀔 때처럼 상태/환경 변수를 분리해 확인하는 접근이 도움이 됩니다.
6) int8 대신 uint8 모델을 기대하는 런타임과의 불일치
흔한 증상/에러
- 안드로이드/임베디드 코드에서 전처리 로직이
uint8기준으로 작성됨 - 결과가 심하게 틀어지거나, 입력 범위 클리핑으로 정보 손실
원인
TFLite 양자화 모델은 입력 타입이 int8 또는 uint8 둘 다 가능하지만, 둘의 해석이 다릅니다.
uint8: 보통zero_point가 128 근처int8:zero_point가 0 근처인 경우가 많음
런타임 코드가 uint8 전제(예: 이미지 픽셀 0~255)로 작성되어 있는데 모델은 int8 이면 스케일 적용이 어긋납니다.
해결
- 앱/서버 전처리 로직을 모델의
input_details["dtype"]에 맞춰 분기 - 팀 내에서 “입력은 int8로 통일” 또는 “uint8로 통일” 규약을 정하고 export 옵션을 고정
in_detail = interpreter.get_input_details()[0]
print(in_detail["dtype"], in_detail["quantization"]) # dtype과 (scale, zero_point) 확인
7) 변환은 성공했는데 delegate에서 크래시 또는 Prepare 실패
흔한 증상/에러
- CPU에서는 동작, GPU/NNAPI delegate에서 실패
Delegate failed to prepare- 모바일 특정 기기에서만 크래시
원인
INT8 모델이라도 실제 실행은 delegate가 담당하는데, delegate별 지원 op/제약이 다릅니다.
- NNAPI는 기기 드라이버/OS 버전에 따라 지원 범위가 달라짐
- GPU delegate는 INT8 지원이 제한적이거나 float16 경로를 선호
- 일부 op는 CPU 커널은 안정적이지만 delegate 커널은 미지원
해결
- 우선 CPU로 강제 실행해 모델 자체 문제인지 분리
- delegate별 지원 op 목록 확인 후, 문제 op를 float fallback하거나 모델 수정
- 모바일에서는 “완전 INT8”보다 “내부 INT8 + 입출력 float” 구성이 더 안정적인 경우가 많음
안정성 관점에서는 네트워크 장애 대응처럼 “타임아웃/폴백 경로”를 설계하는 것이 중요합니다. 예를 들어 delegate 실패 시 CPU로 자동 폴백하는 구조는 gRPC에서 실패 원인을 분리하는 접근과 유사합니다. 관련 사고 대응 방식은 Go gRPC context deadline exceeded 원인·해결 같은 글의 체크리스트 사고방식이 그대로 적용됩니다.
디버깅 체크리스트: 실패를 빠르게 좁히는 순서
INT8 양자화 실패는 원인이 겹치는 경우가 많아, 아래 순서로 “가장 값싼 확인”부터 진행하는 것이 효율적입니다.
representative_dataset를 실제 데이터로 100~500 샘플 구성했는가- 입력 개수/순서/shape/dtype이 정확히 맞는가
supported_ops에서 INT8만 강제했을 때 실패하는 op 이름은 무엇인가- 우선
TFLITE_BUILTINS를 추가해 float fallback으로 변환되는지 확인 - 입출력 dtype 강제를 잠시 제거해 변환/정확도를 확인
- 변환된 모델의 input quantization 파라미터(scale, zero_point)를 런타임에서 적용했는가
- CPU에서 먼저 검증 후 delegate를 붙였는가
마무리
TensorFlow Lite INT8 양자화는 “옵션 몇 개 켜면 끝”이 아니라, 모델 구조·대표 데이터셋·입출력 인터페이스·실행 delegate까지 하나의 시스템으로 봐야 안정화됩니다.
이 글의 7가지 에러 패턴 중에서 가장 먼저 의심해야 할 것은 대표 데이터셋 품질과 입력/출력 dtype 불일치입니다. 변환 실패를 단순히 해결하는 것보다, 캘리브레이션과 런타임 전처리를 정확히 맞추는 것이 실제 정확도와 운영 안정성을 결정합니다.
원하면 다음 정보를 주면, 현재 모델/로그 기준으로 “7가지 중 어디에 해당하는지”를 빠르게 분류해서 구체적인 수정안을 제시할 수 있습니다.
- 변환 코드(옵션 포함)
- 전체 에러 로그(가능하면 원문)
- 모델 입력 shape/dtype, 대표 데이터셋 구성 방식
- 목표 런타임(안드로이드 CPU, NNAPI, iOS CoreML delegate 등)