블로그 문제 해결

whisper.cpp 버전 설정이 안 먹을 때 — 옵셔널 체이닝 폴백과 Metal GPU 타임아웃

설정 파일에 적은 whisper.cpp 버전이 옵셔널 체이닝 폴백 때문에 무시되던 원인과, 그 덕분에 Metal GPU 타임아웃을 피하고 있던 구조를 다룹니다. 실제로 도는 바이너리를 확인하는 순서와 whisper-cli -ng 로 바꿀 때의 조건을 적었습니다.

영상 렌더 파이프라인의 받아쓰기 단계(whisper.cpp)를 손보다가, 설정 파일에 적어 둔 Whisper 버전이 한 번도 쓰이지 않았다는 걸 알게 됐습니다. 바이너리 경로를 고르는 코드가 옵셔널 체이닝 뒤에서 조용히 옛 바이너리로 넘어가고 있었습니다. 그런데 그 옛 바이너리가 GPU(Metal) 없이 빌드된 CPU 전용이라, 따로 시험하다 만났던 GPU Timeout Error 를 우연히 피하고 있었습니다. 설정 버그만 고치면 새 바이너리가 GPU를 켠 채로 돌게 되므로, 바꿀 때는 -ng 옵션으로 GPU를 꺼야 합니다.

아래는 실제로 도는 바이너리를 확인한 순서, GPU를 켰을 때 생긴 문제, 그리고 새 바이너리로 바꿀 때 지키기로 한 조건입니다.

설정한 Whisper 버전이 무시되는 원인

실행할 바이너리 경로를 정하는 코드는 이렇게 되어 있었습니다.

const bin = mod.getWhisperExecutablePath?.(whisperDir, WHISPER_VERSION) ?? path.join(whisperDir, 'main');

설치해 쓰던 @remotion/install-whisper-cpp 버전에는 getWhisperExecutablePath 라는 함수가 없었습니다. 함수가 없으면 ?.() 는 에러 없이 undefined 를 돌려주고, 코드는 ?? 뒤의 .whisper/main 경로로 넘어갑니다.

설정 파일에는 WHISPER_VERSION=1.7.4 가 적혀 있었지만, 실제로 돌던 건 그보다 먼저 make 로 빌드해 둔 1.5.5 바이너리(main)였습니다. 다음 날 cmake 로 빌드한 1.7.4 whisper-cli 는 한 번도 실행되지 않았습니다. 값을 뭐라고 적든 그 값으로 경로를 만들 함수가 없으니 설정은 매번 무시됐습니다. 옵셔널 체이닝은 “없으면 넘어간다”는 문법이라, 함수가 없다는 사실이 에러로 올라오지 않습니다.

실제로 도는 바이너리 확인하는 순서

설정값이 정말 반영되는지는 코드를 읽는 것보다 실행해서 보는 게 빠릅니다.

  1. 함수가 있는지 확인 — typeof mod.getWhisperExecutablePath 를 찍어 봅니다. undefined 가 나오면 옵셔널 체이닝 뒤는 항상 폴백입니다. 이번에도 여기서 undefined 가 나왔습니다.
  2. 선택된 경로를 로그로 남기기 — 경로를 정한 직후 console.log(bin) 을 한 줄 넣고, 설정에서 기대한 경로와 같은지 봅니다.
  3. 바이너리가 아는 옵션으로 구분하기 — 버전마다 지원하는 옵션이 다르면 그걸로도 구분됩니다. 이번에는 main 의 dtw 프리셋에 turbo 모델이 없고 whisper-cli 에는 있어서, turbo 모델을 turbo 를 모르는 바이너리로 돌리고 있었다는 걸 알 수 있었습니다. 동작은 했고 받아쓰기 품질도 나쁘지 않았습니다.
  4. 설정을 일부러 틀린 값으로 바꿔 보기 — 존재하지 않는 버전을 넣어도 아무 일이 없으면 그 설정은 읽히지 않는 값입니다.

옵셔널 체이닝 말고도 try/catch 로 예외를 삼키는 코드, 기본값을 넣는 ||·?? 도 같은 식으로 설정을 가립니다. 폴백이 조용히 도는 문제는 폴백은 평소에 안 돌면 폴백이 아니었다에서 따로 다뤘습니다.

Metal GPU 를 켜면 긴 음성에서 나는 GPU Timeout Error

같은 시기에 GPU 가속도 따로 시험해 봤습니다. whisper-cli 는 Metal, 즉 GPU 가속을 기본으로 켜고 돕니다. 60초 음성 기준 실측은 이렇습니다.

실행 방식60초 음성 처리 시간
CPU62초
GPU(Metal)8초 (약 7배)

짧은 음성은 이렇게 빨라졌지만, 긴 음성에서는 GPU Timeout Error 가 나면서 작업이 통째로 죽었습니다. 328초짜리 음성에서 실패했고, 나중에는 30초짜리에서도 실패했습니다. 파이프라인이 다루는 음성은 대부분 몇 분짜리라, GPU는 쓰지 않기로 했습니다. CPU 전용으로 돌리면 6분 음성에 약 6분, 음성 길이와 비슷한 시간이 걸립니다.

두 버그가 서로를 가리고 있던 구조

두 결과를 같이 놓고 보니 앞뒤가 맞았습니다. 실제로 돌던 1.5.5 main 은 Metal 없이 빌드된 CPU 전용이었고, 그래서 느린 대신 음성 길이와 상관없이 끝까지 돌았습니다.

  • 버그 A: 죽은 설정값 때문에 항상 옛 바이너리로 폴백
  • 버그 B: whisper-cli 의 GPU 백엔드가 긴 음성에서 타임아웃

A 때문에 GPU가 없는 바이너리만 돌았으니 B가 실제 작업에서 일어날 수가 없었습니다. 둘 다 버그인데 서로를 상쇄하는 방향으로 겹쳐 있었고, A만 고치면 whisper-cli 가 Metal을 켠 채로 돌면서 B가 실제 작업에 들어옵니다. “버그니까 고친다”로 끝낼 수 없는 이유가 여기 있었습니다.

죽은 설정값이 옛 CPU 전용 바이너리를 붙잡아 GPU 타임아웃을 피하고 있었고, 설정만 고치면 GPU가 켜져 긴 음성에서 실패하므로 -ng 를 함께 넣어야 하는 구조를 그린 그림

whisper-cli 에서 -ng 로 GPU 끄기

새 바이너리를 쓰려면 바이너리 선택 버그를 고치고, -ng 옵션을 함께 넣어 GPU를 꺼야 합니다.

whisper-cli -ng ...

-ng 는 --no-gpu 의 짧은 형태로, Metal을 쓰지 않고 CPU로 처리하게 합니다. 60초 구간으로 재 보니 whisper-cli -ng 가 48초, 지금 쓰는 main 이 59초였습니다(약 19% 단축). 자막 구간 49개의 경계와 텍스트는 두 바이너리가 완전히 같았습니다.

새 바이너리로 바로 바꾸지 않은 이유

19% 단축이 확인됐는데도 전환은 아직 하지 않았습니다. 위 측정은 60초 구간이었고, GPU 타임아웃이 터졌던 게 바로 긴 음성이었습니다. 프로덕션 받아쓰기 바이너리를 바꾸는 일이라 긴 음성 검증 없이 넣으면 다음 영상의 받아쓰기가 실패할 수 있고, 그 검증 자체가 8분 음성 기준 약 13분 동안 서버에 부하를 줍니다. 그래서 전환 순서를 적어 두고 서버가 한가할 때 하기로 했습니다.

  1. 서버에서 영상 작업이 돌지 않을 때, 미리 알리고 시작합니다
  2. 바이너리 선택 버그를 고쳐 whisper-cli 가 실제로 실행되게 합니다
  3. -ng 를 반드시 함께 넣습니다
  4. 8분 이상 실제 음성으로 돌려서 끝까지 도는지, 텍스트가 main 출력과 같은지, 시간이 줄었는지 봅니다
  5. 셋 다 통과하면 전환하고, 하나라도 아니면 main 경로로 되돌립니다

잘 돌던 버전 설정을 고치기 전에 확인할 것

잘 돌던 부분을 고칠 때는, 고치기 전에 “지금 이게 왜 문제없이 도는가”를 먼저 적어 봅니다.

  • 설정값이 실제 동작에 반영되는지 (위 확인 순서 1~4)
  • 지금 돌고 있는 바이너리·버전이 무엇인지
  • 그 버전에 빠진 기능(이번에는 GPU 가속)이 다른 문제를 막고 있지 않은지
  • 고친 뒤 짧은 입력과 긴 입력을 둘 다 넣어 봤는지

이번에도 60초 구간 결과만 보고 바꿨다면 긴 음성에서 다시 타임아웃을 만났을 겁니다. 에러 없이 실패가 숨어 있던 다른 사례는 조용한 실패가 제일 비싸다에 적어 두었습니다.

자주 묻는 질문

설정값을 바꿔도 동작이 안 변하면 어디부터 봐야 하나요?

설정을 읽는 코드가 실제로 그 값을 반영하는지부터 확인해야 합니다. 옵셔널 체이닝이나 예외 무시(try/catch) 뒤에서 조용히 폴백이 일어나면 설정은 살아 있어 보여도 죽은 값입니다. 로그를 찍어 실제로 어떤 경로가 선택됐는지 눈으로 확인하는 게 가장 빠릅니다.

Whisper에서 GPU(Metal)를 켜면 왜 긴 음성에서 타임아웃이 나나요?

짧은 음성은 GPU 가속으로 훨씬 빨리 끝나지만, 긴 음성은 처리 시간이 길어지면서 GPU 백엔드가 정해진 시간 안에 응답하지 못해 타임아웃으로 죽는 경우가 있습니다. 이럴 때는 -ng 옵션으로 GPU를 끄고 CPU로 처리하면 느리지만 안정적으로 완료됩니다.

지금 잘 돌아가는 시스템을 굳이 파봐야 하나요?

잘 돌아간다고 해서 의도대로 돌아가는 것은 아닙니다. 왜 잘 도는지 설명할 수 없다면, 그건 우연히 두 문제가 상쇄된 상태일 수 있습니다. 최소한 "왜 지금 괜찮은가"를 한 문장으로 설명할 수 있을 때까지는 확인해 두는 편이 안전합니다.

서로 상쇄된 두 버그는 어떻게 발견하나요?

보통 하나를 "고쳤는데" 갑자기 멀쩡하던 기능이 깨질 때 드러납니다. 한쪽을 수정한 순간 다른 쪽 버그가 노출되기 때문입니다. 수정 전후로 동작이 이상하게 뒤집히면, 방금 건드린 값이 다른 버그를 가려주고 있었는지 의심해 보는 게 좋습니다.

#디버깅#설정관리#GPU#1인 개발#whisper