ffmpeg CPU 906%→74% — VideoToolbox 하드웨어 인코딩 적용기
쇼츠 자동화 스킬을 만들어 한동안 잘 쓰고 있었습니다. 자막을 입히고, 무음 구간을 잘라내고, 여러 영상을 크로스페이드로 이어 붙이는 일을 한 번에 처리해 주는 파이프라인입니다. 동작 원리는 앞선 글 「whisper와 Claude로 영상 자막·무음 컷 자동화하기 — 동작 원리」에 정리해 두었는데, 이 글은 그 후속편이라고 보시면 됩니다. 잘 돌아가긴 했지만, 영상을 렌더링할 때마다 CPU 사용률이 치솟는 장면이 늘 마음에 걸렸습니다.
렌더링이 시작되면 CPU 사용률이 700%, 800%를 넘어 한번은 906%까지 치솟았습니다. 노트북이 뜨거워지고 팬이 돌기 시작하면, 12초짜리 짧은 클립 하나를 처리하는 데 이렇게까지 무리를 해야 하나 싶은 생각이 들었습니다. 그래서 가장 먼저 든 생각은 이것이었습니다. 메모리를 더 활용해서 CPU 부담을 줄일 수는 없을까?
결론부터 말하면, 그 방향은 처음부터 틀렸습니다. 문제는 메모리가 아니라, 인코딩이라는 일을 누구에게 시키느냐에 있었습니다.
메모리로 풀 문제가 아니었습니다
우선 700%라는 숫자부터 짚고 가겠습니다. macOS의 CPU 사용률은 코어별 사용률을 합산해서 표기합니다. 그러니 700%는 "CPU가 700% 고장 났다"는 뜻이 아니라, 7개 코어가 동시에 100%로 일하고 있다는 뜻입니다. 제 환경은 Apple M2 Pro(성능 8 + 효율 4 코어)라서, 906%는 성능 코어를 거의 다 끌어다 쓰고 있다는 이야기였습니다.
그제야 제가 잘못 짚었다는 걸 깨달았습니다. 비디오 인코딩은 본질적으로 compute-bound, 즉 메모리가 아니라 계산량이 병목인 작업입니다. 메모리를 더 쓴다고 해서 계산할 양이 줄어드는 게 아니기 때문에, "메모리를 더 쓰면 CPU가 내려간다"는 트레이드오프 자체가 성립하지 않습니다. 곰곰이 생각해보니, 메모리는 애초에 이 문제의 등장인물이 아니었습니다.
진짜 범인은 인코더 설정에 있었습니다. 기존 파이프라인은 libx264 -preset medium으로 인코딩하고 있었는데, 이건 CPU가 직접 계산하는 소프트웨어 인코딩입니다. 화질과 압축률을 높이려고 CPU 성능 코어를 거의 전부 동원하니, 사용률이 그렇게 높게 찍히는 게 오히려 정상이었습니다. 버그가 아니라 설계대로 동작하고 있었던 셈입니다. 게다가 이 파이프라인은 정규화 → 무음 컷 → 연결 → 자막 번인으로 영상을 여러 번 다시 인코딩하기 때문에, 그 시간 내내 CPU가 포화 상태에 머물렀습니다.
인코딩을 CPU가 아니라 전용 하드웨어에 넘기기
다행히 Apple Silicon에는 CPU와 별개로 전용 미디어 엔진이 들어 있습니다. 영상 인코딩과 디코딩만 전담하는 하드웨어 블록인데, 이 일에 특화되어 있어서 같은 작업을 훨씬 적은 전력과 시간으로 처리합니다. ffmpeg에서는 h264_videotoolbox 인코더가 이 엔진을 사용합니다. CPU가 직접 계산하던 소프트웨어 인코딩을, 전용 칩이 처리하는 하드웨어 인코딩으로 넘기는 셈입니다. 인코딩을 CPU 코어가 아니라 이 미디어 엔진으로 넘기면, CPU는 거의 손을 놓게 됩니다.
실제로 제가 자막 번인 패스에서 쓰는 명령은 이렇게 바뀌었습니다.
ffmpeg -i 입력.mp4 -vf "ass=cap.ass,format=yuv420p" \
-c:v h264_videotoolbox -profile:v high -b:v 14M -spatial_aq 1 -g 30 \
-c:a copy -movflags +faststart 출력.mp4
핵심은 -c:v를 h264_videotoolbox로 바꾼 것 하나지만, 인코더를 바꾸면 함께 손봐야 하는 옵션이 있습니다. libx264 전용 옵션은 VideoToolbox에서 무시되거나 경고만 띄우기 때문입니다.
- 그대로 두는 것:
-profile:v high(유튜브가 권장하는 H.264 High 프로파일),-g 30(closed GOP),-movflags +faststart는 인코더와 무관하게 의미가 있어 유지합니다. - 빼는 것:
-crf,-preset,-bf는libx264전용이라 VideoToolbox에서는 효과가 없습니다. B프레임은 VideoToolbox가 기본으로 처리하므로-bf도 필요 없습니다.
화질은 두 가지 방식으로 제어할 수 있습니다. 품질을 우선한다면 비트레이트를 직접 지정하는 -b:v 14M이 무난하고, 용량을 줄이고 싶다면 상수 품질 모드인 -q:v 63을 씁니다. 다만 -q:v는 60 밑으로는 내리지 않는 편이 좋습니다. 그 이유는 잠시 뒤 실측에서 다시 다루겠습니다. 오디오는 인코딩에 CPU를 거의 쓰지 않으므로 기존 -c:a aac나 위처럼 -c:a copy 정도로 충분하고, 굳이 하드웨어로 넘기고 싶다면 AudioToolbox를 쓰는 -c:a aac_at 옵션도 있습니다.
수치의 근거 — 어떻게 쟀는가
인코더를 바꾸기로 마음먹기 전에, 정말 이득인지부터 확인하고 싶었습니다. 눈대중으로 "빨라진 것 같다"고 말하고 싶지는 않았습니다. CPU 사용률은 /usr/bin/time -p로 한 번 돌려 벽시계(real)·user·sys 시간을 받은 뒤, 평균 CPU%를 (user + sys) / real × 100으로 계산했습니다. top이나 ps로 눈대중 샘플링하는 것보다 전 구간 평균이라 더 정직한 숫자입니다. 화질은 주관적 인상 대신 VMAF(0~100의 지각 품질 점수)를 썼는데, 같은 필터를 거친 프레임을 무손실(libx264 -qp 0)로 한 벌 만들어 기준으로 두고 각 후보를 그 무손실과 비교했습니다. VMAF는 대략 6점 차이가 사람이 인지하는 한계로 알려져 있습니다.
한 가지 덧붙이자면, 측정은 반드시 한 번에 하나씩 돌려야 합니다. 처음엔 시간을 아끼려고 두 인코딩을 동시에 돌렸다가 CPU 경합으로 숫자가 오염되어, 결국 다시 측정해야 했습니다. 같은 소스 구간·같은 필터 체인에 오디오는 제거(-an)해 비디오 인코딩만 격리한 결과는 다음과 같았습니다. 환경은 M2 Pro, ffmpeg-full, 1080×1920 60fps, 12초 클립의 정규화 패스입니다.
| 인코더 설정 | 평균 CPU | 렌더 시간 | VMAF (무손실 기준) |
|---|---|---|---|
libx264 -preset medium -crf 18 (기존) |
906% | 4.96s | 96.3 (기준) |
h264_videotoolbox -b:v 12M |
74% | 4.57s | 96.0 (사실상 동일) |
h264_videotoolbox -q:v 62 |
82% | 4.23s | 94.5 (육안 무차이, 파일 더 작음) |
libx264 medium + -threads 4 (CPU 캡) |
237% | 3.6배 느림 | 96.3 (화질 동일) |
요약하면 CPU는 약 11배 줄었고, 렌더는 오히려 더 빨라졌으며, 화질은 육안으로 구분되지 않았습니다. 처음 메모리를 떠올렸던 게 무색할 만큼, 답은 다른 곳에 있었습니다.
그래서 무엇을 잃는가
이 정도면 만능처럼 보이지만, 정직하게 짚어야 할 한계가 있습니다. 같은 파일 크기를 기준으로 비교하면, 압축 효율은 여전히 libx264 medium이 조금 더 좋습니다. 소프트웨어 인코딩이 시간을 더 들여 더 영리하게 압축하기 때문입니다. 다만 비트레이트를 조금 올리면(-b:v 14M) 그 차이는 육안으로 사라지고, 대신 치르는 비용은 CPU가 아니라 디스크 용량입니다. CPU를 11배 아끼는 대가로 용량을 조금 더 쓰는 거래라면, 저로서는 망설일 이유가 없었습니다.
여기서 결정적인 사실이 하나 있습니다. 유튜브는 업로드된 영상을 어차피 전부 다시 인코딩합니다. 그러니 로컬 파일이 가진 미세한 화질 우위는 시청자 화면까지 거의 전달되지 않습니다. 충분히 높은 비트레이트의 깨끗한 소스만 올려 주면 그것으로 족하다는 뜻입니다. 로컬에서 화질을 한 톨까지 짜내는 일은, 적어도 이 파이프라인에서는 큰 의미가 없었습니다.
몇 가지 실무적인 주의도 함께 남깁니다. 앞서 미뤄 둔 이야기인데, -q:v는 60 밑으로 내리지 않는 편이 좋습니다. 실측에서 -q:v 50은 VMAF 89.9로 눈에 띄게 떨어졌습니다. 그리고 화질을 단 한 톨도 바꾸고 싶지 않은 경우라면, libx264를 유지하면서 -threads로 CPU만 제한하는 방법이 차선책입니다. 다만 표에서 보듯 3~4배 느려지는 대가를 치러야 합니다. 평소 작업이라면 VideoToolbox가 정답에 가깝다는 생각이 들었습니다.
결국, 누구에게 일을 시킬 것인가
돌이켜 생각해보면, 제가 메모리로 풀어보려던 문제는 사실 일을 CPU에서 전용 하드웨어로 넘기는 문제였습니다. 자원을 더 투입하는 방향이 아니라, 그 일에 가장 적합한 일꾼에게 맡기는 방향이었던 셈입니다. 문제를 잘못 정의하면 아무리 열심히 풀어도 엉뚱한 답에 도달한다는 것을, 906%라는 숫자가 다시 한번 일러 주었습니다.
한편으로 이 결론은 앞선 자동화 글과 묘하게 같은 결을 띱니다. 무거운 계산은 ffmpeg의 하드웨어 인코더 같은 결정적 도구에 맡기고, 사람과 세션은 "어떤 설정이 적절한가"를 판단하는 데 집중하는 분업 말입니다. 결국 좋은 자동화란 모든 걸 직접 짊어지는 게 아니라, 각자 가장 잘하는 일을 하도록 일을 나누는 것이라는 생각이 듭니다. CPU를 혹사하던 파이프라인을 고치며, 의외로 그런 단순한 원리를 다시 확인하게 되었습니다.
관련 글
Vrew 하드웨어 가속, 켜야 하나 꺼야 하나 — 지인 노트북에 SSH 로 들어가 확인한 13개 sub-feature 이야기
"Vrew 의 하드웨어 가속, 켜야 하나 꺼야 하나" 라는 한 줄짜리 질문에서 시작해, 지인 노트북에 SSH 로 들어가 Chromium 의 13개 sub-feature 와 4개 레이어를 직접 확인해본 기록. NVENC 가 살아 있는 환경의 결론과, 흔한 오해.
whisper와 Claude로 영상 자막·무음 컷 자동화하기 — 동작 원리
자기소개 쇼츠 하나를 만들려다, 자막과 무음 컷을 외부 SaaS 없이 자동화한 기록입니다. whisper·ffmpeg 같은 결정적 도구가 타이밍과 렌더링을, 옆에 켜둔 대화 세션의 Claude가 '어디서 자르고 묶을지'라는 판단을 맡는 분업의 동작 원리를 풀어냈습니다.
영상에서 텍스트를 뽑고 싶었다 — ffmpeg, whisper-cpp, VAD로 로컬 STT 파이프라인 만들기
영상 파일에서 텍스트를 추출하는 로컬 STT 파이프라인을 구축한 경험입니다. ffmpeg가 오디오를 왜 16kHz 모노 PCM으로 변환하는지, whisper-cpp가 Python 없이 어떻게 동작하는지, VAD가 STT 품질을 어떻게 개선하는지 하나씩 따라가봅니다.