pile·
AI / ML·스캐터랩스캐터랩 (이루다)·

TensorFlow Custom Op으로 데이터 변환 최적화하기

대규모 언어 모델용 TFRecord를 만들 때 N-gram masking 위치 계산이 전체 전처리의 절반 이상을 차지한 문제를 TensorFlow C++ Custom Op으로 해결한 사례다. Op metadata, kernel 구현, shared object 빌드, Python 로딩 과정을 코드 수준에서 설명한다. 병목은 문장당 4.697ms에서 17.147μs로 줄어 약 250배 빨라졌다.

핵심 포인트
  • Custom Op은 기존 TensorFlow op 조합으로 표현하기 어렵거나 비효율적인 연산, 직접 fusion할 연산에 적합하다.
  • BERT 입력과 NSP·SOP label을 만드는 전처리에서 확률적 N-gram masking 후보 계산이 가장 큰 병목이었다.
  • `REGISTER_OP`로 input·output·attribute·shape function을 선언하고 `OpKernel.Compute()`에 C++ 계산을 구현한다.
  • `REGISTER_KERNEL_BUILDER`로 metadata와 CPU kernel을 연결한 뒤 TensorFlow compile·link flag를 사용해 `.so`로 빌드한다.
  • `tf.load_op_library()`로 shared object를 읽으면 PascalCase op가 snake_case Python 함수로 노출된다.
  • 유지보수와 디버깅 비용이 크므로 일반 op으로 충분한 경우 피하고, 측정된 핵심 병목에 제한적으로 적용해야 한다.
상세 정리
  • 상황: 방대한 corpus를 TPU 학습용 TFRecord로 변환해야 했다. 결과에는 BERT의 input ids·attention mask, masking 위치, NSP 또는 SOP label이 들어간다.
  • 동적 masking: 여러 epoch마다 다른 위치를 가리기 위해 실제 masked input을 저장하지 않고 masking position만 기록했다. ALBERT 방식처럼 연속 token을 가리는 N-gram masking도 적용했다.
  • 기존 알고리즘: mask 가능한 sequence를 확률에 따라 N-gram으로 자르고 후보를 shuffle한 뒤 필요한 token 수만큼 선택했다. TensorFlow의 `while_loop`와 여러 op로 구현하자 이 단계가 전처리 시간의 절반 이상을 썼다.
  • 적용 기준: Custom Op은 C++ 구현 비용이 있지만 원하는 연산을 하나의 효율적인 kernel로 만들 수 있다. 이 사례는 기존 op 조합의 제어 흐름과 중간 연산 비용이 큰 경우에 해당했다.
  • Metadata: `REGISTER_OP("GetMaskingPosition")`에 세 input, output, seed attribute와 문서를 선언했다. `SetShapeFn`은 input shape를 그대로 output shape로 전달했다.
  • Kernel 구현: `OpKernel`을 상속하고 `Compute`를 override했다. `context->input(index)`로 tensor reference를 받고 `flat<T>()` handle을 통해 값을 읽고 썼다.
  • Output 할당: `allocate_output`에 순번과 shape를 넘겨 Tensor pointer를 얻고, 같은 방식으로 flat handle을 만들어 계산 결과를 채웠다.
  • 장치 등록: `REGISTER_KERNEL_BUILDER(Name(...).Device(DEVICE_CPU), ...)`로 선언과 구현을 연결했다. 데이터 전처리용이라 CPU kernel로 지정했고 GPU 연산이면 별도 device 구현이 필요하다.
  • 빌드: `tf.sysconfig.get_compile_flags()`와 `get_link_flags()`로 현재 TensorFlow 설치와 맞는 option을 얻었다. g++에서 C++11, shared, fPIC, O2 옵션으로 `.so`를 생성했다.
  • Python 연동: `tf.load_op_library(path).get_masking_position`으로 함수를 가져왔다. Python 함수처럼 tensor와 `max_n`, `num_tokens_to_mask`를 전달했다.
  • 벤치마크: sequence length 128, 최대 N-gram 3 조건에서 100회 실행했다. TensorFlow 구현은 총 0.4697초, Custom Op은 0.001714초였다.
  • 효과: 문장 하나의 병목이 4.697ms에서 17.147μs로 줄었다. 가장 느렸던 masking 함수가 전체 변환 과정에서 가장 빠른 연산이 됐다.
  • Trade-off: Bazel 없이 유지보수하기 어렵고 op 이해와 debugging 시간이 커진다. 반면 model kernel fusion이나 CUDA backward까지 포함한 핵심 연산에는 DeepSpeed처럼 큰 최적화 여지가 있다.
왜 읽나TensorFlow profiler로 확인한 병목을 C++ kernel로 내릴 가치가 있는지 판단하고 실제 빌드·연동 절차를 알고 싶은 ML 시스템 엔지니어에게 적합하다.
스캐터랩
스캐터랩 (이루다) 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. AI / ML·LY CorporationLY Corporation·

    Grafana에서 자연어로 장애 원인을 분석하기: LLM 에이전트 기반 SRELens 개발기

    LY Corporation Home SRE 팀이 장애 분석 시 메트릭·로그·트레이스가 각각 다른 화면에 흩어져 있는 문제를 해결하기 위해 Grafana 플러그인 SRELens를 개발했다. SRELens는 LLM 에이전트가 자연어 질의를 받아 실제 관측성 데이터를 조회하고, 근거와 함께 장애 원인 후보를 정리해 주는 도구다. LGTM-P 스택(Loki·Grafana·Tempo·Mimir·Pyroscope)과 FlavaMCP 게이트웨이를 통합해 단일 채팅 인터페이스에서 멀티시그널 분석이 가능하다.

    #llm-app#mcp#observability+2