- Published on
TensorFlow Lite 변환 실패, Flex·Select Ops로 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙용 SavedModel이나 Keras 모델을 모바일/엣지에 올리려다 TFLiteConverter에서 막히는 경우가 많습니다. 에러 메시지는 대개 두 부류로 나뉩니다.
- 변환 단계에서 바로 실패: 특정 TF 연산이 TFLite로 내려오지 못함
- 변환은 성공했는데 실행 시점에 실패:
SELECT_TF_OPS가 없거나, Flex delegate 미포함으로 커널을 찾지 못함
이 글은 “Flex(= Select TF Ops)로 빠르게 살리는 방법”과 “가능하면 TFLite 내장 연산만 쓰도록 모델을 정리하는 방법”을 함께 다룹니다. 운영 관점에서 보면 Flex는 응급처치에 가깝고, 최종 목표는 내장 연산으로 수렴시키는 것입니다.
왜 TFLite 변환이 실패하나: 핵심 원리
TFLite는 기본적으로 경량 커널 집합만 제공합니다. 반면 TensorFlow 그래프에는 다음이 섞여 있을 수 있습니다.
- TF 전용 연산(예:
tf.image계열 일부, 문자열/해시 관련, 복잡한 control flow) - 학습 편의용 연산(예: 특정 정규화/손실/메트릭이 그래프에 남는 경우)
- 동적 shape, ragged/sparse 텐서, 변환기에서 정적으로 풀기 어려운 패턴
그래서 변환기는 “이 연산을 TFLite builtin으로 매핑할 수 없다”라고 판단하면 실패합니다. 여기서 선택지는 둘입니다.
- Select TF Ops(= Flex) 활성화로, 변환 불가능한 연산을 TF 런타임 커널로 실행하게 한다
- 모델을 수정/재작성해서 TFLite builtin만으로 동작하게 만든다
Flex와 Select TF Ops: 개념 정리
- Select TF Ops: 변환 시
supported_ops에SELECT_TF_OPS를 포함해, 일부 연산을 “TF 연산 그대로” TFLite 모델에 남기는 옵션 - Flex delegate: 런타임에서 그 TF 연산들을 실행하기 위해 필요한 실행 엔진
즉, 변환 옵션만 켠다고 끝이 아닙니다. 앱(안드로이드/iOS) 빌드에 Flex 런타임이 포함되어야 실제로 돌아갑니다. 이 때문에 “변환은 성공했는데 폰에서 크래시”가 자주 발생합니다.
1단계: 변환 실패 로그에서 미지원 Op를 정확히 뽑기
변환 실패 시 로그에 Some ops are not supported by the native TFLite runtime 같은 문구와 함께 미지원 연산 리스트가 나오는 경우가 많습니다. 우선 재현 가능한 최소 코드로 변환을 돌려보세요.
import tensorflow as tf
saved_model_dir = "./saved_model"
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
# 기본은 TFLITE_BUILTINS만 사용
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
try:
tflite_model = converter.convert()
open("model.tflite", "wb").write(tflite_model)
print("OK")
except Exception as e:
print("CONVERT FAIL:", e)
여기서 실패하면, 다음 단계로 Flex를 켜서 “일단 돌아가게” 만들고, 그다음 어떤 연산이 Flex로 남았는지 확인하는 흐름이 좋습니다.
2단계: 응급 처치 - Select TF Ops로 변환 성공시키기
SELECT_TF_OPS를 추가하면, 변환기가 builtin으로 못 내리는 연산을 TF 연산으로 남겨둡니다.
import tensorflow as tf
saved_model_dir = "./saved_model"
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.target_spec.supported_ops = [
tf.lite.OpsSet.TFLITE_BUILTINS,
tf.lite.OpsSet.SELECT_TF_OPS,
]
# 경우에 따라 필요: 텐서 리스트/동적 패턴이 섞인 모델
converter._experimental_lower_tensor_list_ops = False
tflite_model = converter.convert()
open("model_select_ops.tflite", "wb").write(tflite_model)
print("converted with SELECT_TF_OPS")
이렇게 하면 변환 성공률은 확 올라갑니다. 하지만 다음 리스크가 생깁니다.
- 바이너리 크기 증가(특히 모바일)
- 런타임 의존성 증가(Flex delegate)
- 성능/가속 경로 제한(GPU delegate 등과 궁합 이슈)
그래서 운영 제품이라면 “Flex로 남은 연산이 무엇인지”를 반드시 확인해야 합니다.
3단계: 어떤 연산이 Flex로 남았는지 확인하기
TFLite 모델 안에 Flex 커스텀 연산이 들어갔는지 확인하면 됩니다. 파이썬에서 인터프리터로 간단히 점검할 수 있습니다.
import tensorflow as tf
interpreter = tf.lite.Interpreter(model_path="model_select_ops.tflite")
interpreter.allocate_tensors()
ops = interpreter._get_ops_details()
flex_ops = [op for op in ops if "Flex" in op.get("op_name", "")]
print("total ops:", len(ops))
print("flex ops:", len(flex_ops))
for op in flex_ops[:30]:
print(op["op_name"], op.get("inputs"), op.get("outputs"))
Flex*가 많을수록 “사실상 TF를 끼고 도는 모델”에 가까워집니다. 이 상태에서 앱 크기/성능/가속 요구사항을 다시 판단해야 합니다.
4단계: 런타임 실패 - Flex delegate 미포함 문제
변환은 됐는데 실행 시 Flex 관련 커널을 못 찾는 에러가 나면, 앱에 Select TF Ops 런타임이 포함되지 않은 겁니다.
Android에서의 포인트
org.tensorflow:tensorflow-lite만 넣으면 builtin만 지원합니다.- Select TF Ops를 쓰려면 보통
tensorflow-lite-select-tf-ops아티팩트를 추가해야 합니다.
Gradle 예시는 다음처럼 구성합니다(버전은 프로젝트에 맞게 고정하세요).
dependencies {
implementation "org.tensorflow:tensorflow-lite:2.15.0"
implementation "org.tensorflow:tensorflow-lite-select-tf-ops:2.15.0"
}
이후에도 크래시가 난다면, ABI/프로가드/R8로 네이티브 라이브러리가 누락되는지 확인해야 합니다. 이런 유형의 원인 추적은 로그를 끝까지 따라가며 재현 환경을 고정하는 게 중요한데, 서비스 환경에서 원인 추적을 체계화하는 접근은 systemd 서비스 무한 재시작 원인과 journalctl 추적 글의 “관측 가능한 로그를 먼저 만들고, 재시작/실패 루프를 끊고, 근거를 쌓는 방식”이 그대로 응용됩니다.
iOS에서의 포인트
iOS는 CocoaPods/SwiftPM 구성에 따라 Select TF Ops가 빠지기 쉽습니다. “TFLite Swift 기본 패키지로는 builtin만 포함”되는 케이스가 있으니, Flex가 필요한 모델이라면 공식 문서의 Select TF Ops 포함 방법을 확인하고, 최종 앱 바이너리에 Flex 관련 심볼이 실제로 들어갔는지 점검해야 합니다.
5단계: Flex를 줄이는 방향으로 모델 정리하기(권장)
Flex는 편하지만, 제품화에서 비용이 큽니다. 가능하면 아래 순서로 builtin 연산만 남기도록 모델을 정리하는 게 좋습니다.
5.1 학습용 노드 제거: inference 전용 그래프 만들기
- Dropout, 학습용 metric, loss 등이 그래프에 남지 않게 export
- Keras라면
training=False경로로 저장
import tensorflow as tf
model = tf.keras.models.load_model("./keras_model")
@tf.function(input_signature=[tf.TensorSpec([None, 224, 224, 3], tf.float32)])
def serving_fn(x):
y = model(x, training=False)
return {"outputs": y}
tf.saved_model.save(model, "./saved_model_clean", signatures=serving_fn)
이렇게 “서빙 시그니처를 단순화”하면 변환기가 다루기 쉬워집니다.
5.2 문제 연산을 TFLite 친화적인 연산으로 치환
자주 발목 잡는 패턴 예시는 다음과 같습니다.
tf.image.non_max_suppression*계열: TFLite에 대응 op가 제한적이거나 버전에 따라 다름- 문자열 처리/토크나이저: TFLite builtin로는 거의 불가(가능하면 앱단에서 처리)
tf.map_fn, 복잡한tf.while_loop: 정적 unroll이 안 되면 실패
대응 전략은 “모델 그래프 밖으로 빼기” 또는 “동등한 수치 연산으로 재구성”입니다. 예를 들어 토크나이징은 앱에서 수행하고, 모델 입력은 int32 토큰 IDs로 받도록 바꾸면 변환 성공률이 크게 올라갑니다.
5.3 동적 shape를 줄이고 입력 크기를 고정
TFLite는 정적 shape에서 가장 안정적입니다. 특히 배치 차원 외의 공간 차원(예: 가변 길이 시퀀스)이 흔들리면 변환/최적화가 어려워집니다.
- NLP: 최대 길이 패딩 후 고정 길이 입력
- CV: 리사이즈를 모델 밖(전처리)으로 빼고 고정 크기 입력
5.4 양자화는 “변환 성공”과 “실행 성공”을 분리해서 접근
양자화는 모델을 더 까다롭게 만들 수 있습니다. 순서는 보통 아래가 안전합니다.
- float 모델을 builtin-only로 변환 성공
- representative dataset을 넣어 int8 양자화 시도
- 정확도/성능/호환성 검증
import tensorflow as tf
import numpy as np
saved_model_dir = "./saved_model_clean"
def rep_data_gen():
for _ in range(100):
x = np.random.rand(1, 224, 224, 3).astype(np.float32)
yield [x]
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = rep_data_gen
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_int8 = converter.convert()
open("model_int8.tflite", "wb").write(tflite_int8)
양자화에서 막히면, 먼저 float로 돌아가게 만든 뒤 어떤 레이어가 양자화 친화적이지 않은지 좁혀가는 편이 디버깅 비용이 낮습니다.
6단계: 디버깅 체크리스트(실전)
변환 단계 체크
- 변환 옵션에서
supported_ops가 builtin-only인지, Select TF Ops가 섞였는지 명확히 구분 - 실패 로그에서 “첫 번째로 등장하는 미지원 op”에 집중(연쇄 오류가 많음)
- SavedModel 시그니처 단순화: 입력/출력 텐서만 남기기
런타임 단계 체크
- 모델에
Flexop가 들어갔는지 확인(앞서 소개한Interpreterop dump) - Android/iOS 빌드에 Select TF Ops 런타임이 실제로 포함됐는지 확인
- 성능 이슈가 있으면 “Flex op 비율”을 먼저 줄이기
운영에서 이런 문제는 “한 번에 다 고치려다 더 크게 망가지는” 경우가 많습니다. 원인을 좁히는 방식(관측 포인트 추가, 최소 재현, 단계적 롤백)은 LangChain 에이전트 무한 루프 끊는 실전 디버깅에서 다루는 디버깅 사고방식과도 닮아 있습니다. 결국 복잡계 디버깅은 동일한 패턴을 따릅니다.
결론: Flex는 빠른 해결책, 목표는 builtin-only
- 당장 변환이 막히면
SELECT_TF_OPS로 변환 성공을 먼저 만든다 - 모델 안에
Flex가 얼마나 남았는지 측정하고, 앱에 Flex 런타임을 포함해 실행 성공까지 확인한다 - 제품 품질(크기/성능/가속/유지보수)을 위해서는 Flex 의존도를 줄이고 TFLite builtin-only로 수렴시키는 리팩터링을 진행한다
마지막으로, 변환 실패를 “옵션 하나로 해결”하려 하기보다, 변환 단계와 런타임 단계를 분리해 관찰하고 체크리스트 기반으로 좁혀가면 해결 속도가 훨씬 빨라집니다. 모델이 커질수록 이 접근이 사실상 유일한 실전 해법입니다.