포스트

맥북에서 AI 영상 생성 모델 돌리기 - MiniMax H3 로컬 실행과 30초 애니메이션 만들기

클라우드도 API도 안 쓰고 맥북 한 대로 30초짜리 애니메이션을 만들었습니다. 영상과 소리가 한 모델에서 같이 나옵니다. 빗소리를 나중에 입힌 게 아니라 처음부터 함께 생성된 겁니다.

소리를 켜고 봐주세요 — 빗소리와 우산 펴지는 소리가 모델이 함께 생성한 것입니다.

🤖 이 영상은 AI 생성물입니다. 영상·오디오 전체가 MiniMax H3 로 생성되었고, 캐릭터 시트와 키프레임은 OpenAI 이미지 생성으로 만들었습니다. Powered by MiniMax H3.

쓴 모델은 MiniMax H3, 실행기는 antirez/h3.c 입니다. Redis 만든 그 antirez 가 Apple Silicon 용으로 짠 Metal 구현체인데, 공식 코드는 NVIDIA 전제라 맥에서 안 돌아갑니다.

앞쪽에 바로 따라할 수 있는 방법을, 뒤쪽에 제가 걸린 함정들을 정리했습니다.

⚠️ 라이선스를 먼저 확인하세요. MiniMax H3 Community License 는 Excluded Territories 에 대한민국을 포함합니다(§I.5). 그리고 §V.4 의 지역 제한이 가중치뿐 아니라 생성된 결과물(Outputs)까지 미칩니다 — use, reproduce, modify, distribute, or display 가 모두 금지 대상입니다. Hugging Face 에서 토큰 없이 받아진다고 해서 써도 된다는 뜻이 아닙니다.

platform.minimax.io/h3-license 에서 개별 인가를 신청하면 됩니다. 저는 신청하고 얼마 지나지 않아 인가를 받았습니다.

필요한 것

  • 통합 메모리 36GB — 15초 렌더 실측 피크 28.2GiB
  • 디스크 144GB — 전체 저장소는 498GB 지만 FL2VA/ 만 받으면 됩니다
  • 전원 96W 이상 ← 이게 함정입니다. 아래 참조
  • Xcode Command Line Tools, ffmpeg

칩은 M5 계열이면 훨씬 빠릅니다. M3 Max 에서도 돌아가지만 약 10배 느립니다. 왜 그런지는 뒤에서 따로 다룹니다.

설치

1
2
git clone https://github.com/antirez/h3.c
cd h3.c && make          # 2.4초면 끝납니다

의존성이 Xcode CLT + ffmpeg 뿐입니다. Metal 셰이더를 런타임에 컴파일해서 별도 툴체인이 필요 없습니다.

체크포인트는 FL2VA/ 만 받습니다.

1
hf download MiniMaxAI/MiniMax-H3 --include "FL2VA/*" --local-dir ./MiniMax-H3

144GB 라 시간이 걸립니다. 저는 1시간 50분 걸렸습니다.

가장 단순한 실행

1
2
./h3 -d ./MiniMax-H3 -p "A red fox walks through fresh snow in a pine forest." \
  --seconds 5 -o out.mp4

옵션은 전부 기본값이 최선입니다. 속도·품질 옵션 5가지를 50회 돌려 재봤는데 결론이 그랬습니다. 비용을 줄이고 싶으면 --steps 하나만 낮추면 됩니다.

768² / 56프레임(2.33초) 기준입니다.

용도 설정 M3 Max M5 Max
컷 탐색 --core-reuse 4 --token-reduction 7.3분
다듬기 --steps 12 20분
납품 기본값 37분 4분

M3 Max 쪽은 --ssd-streaming 을 붙인 값입니다(36GB 에서는 필수). M5 에서는 절감 옵션을 쓸 이유가 거의 없습니다 — 기본값이 이미 4분이라 컷 탐색용 옵션의 절감폭이 수십 초 수준으로 줄어듭니다.

길이는 15초가 최대입니다

프레임 수는 17n + 5 로 올림 정렬되고 362프레임(15.083초)이 상한입니다. 24fps 고정이고 바꿀 수 없습니다. 소스에 상수로 박혀 있습니다.

1
#define H3_FPS 24

--seconds 로 지정하면 계산을 안 해도 됩니다.

지정 실제 프레임 길이
--seconds 5 124 5.17초
--seconds 10 243 10.13초
--seconds 15 362 15.08초
--seconds 16 거부

★ 긴 영상은 나눠 찍는 게 훨씬 쌉니다

이게 이번에 재보고 가장 놀랐던 부분입니다. 프레임 수를 5점(56·124·192·260·362) 실측했더니 시간이 프레임에 대해 지수 약 1.9 로 늘어납니다. 선형이 아닙니다.

프레임 초/step
56 7.7
124 47.1
192 87.4
260 141.1
362 286.6

프레임 수 대비 step 당 시간 - 실측 곡선이 선형 기준선보다 훨씬 빠르게 올라간다 프레임 수 대비 step 당 시간 - 실측 곡선이 선형 기준선보다 훨씬 빠르게 올라간다

주황 점선이 “선형이라면” 입니다. 56프레임의 7.7초를 그대로 비례시키면 362프레임은 49.8초여야 하는데 실측은 286.6초, 5.8배입니다.

프레임이 6.5배인데 비용은 39배입니다. 그래서 같은 30초를 만들 때 이렇게 갈립니다.

구성 상대 비용
15초 × 2클립 55.8
5초 × 6클립 12.4

5초씩 여섯 번 찍는 게 4.5배 쌉니다. 어차피 15초가 상한이라 30초를 만들려면 이어붙여야 하는데, 그럴 거면 잘게 나누는 편이 이득입니다.

캐릭터를 유지하는 방법

텍스트 프롬프트만으로는 캐릭터가 컷마다 달라집니다. --ref-image 라는 옵션이 있지만 Ref2VA/ 체크포인트(별도 144GB) 가 필요합니다. 저는 안 받았습니다.

대신 --first-frame / --last-frameFL2VA/ 로 동작합니다. 시작·끝 이미지를 물려주면 그 사이를 모델이 채웁니다.

순서는 이렇습니다.

1. 캐릭터 시트를 먼저 만듭니다. 저는 OpenAI Codex CLI 의 이미지 생성을 썼습니다.

한 가지 자세만 담으면 안 됩니다. 처음엔 정면 전신 한 장만 만들었는데, 각도가 바뀌는 컷에서 캐릭터가 흔들리고 목이 꺾인 것처럼 나왔습니다. 실제로 쓸 동작을 포함한 여러 포즈로 만들어야 합니다.

Choco 캐릭터 모델 시트 - 6가지 포즈

Mori 캐릭터 모델 시트 - 6가지 포즈

2. 장소 레퍼런스도 만듭니다. 이걸 안 해서 컷마다 다른 가게가 나왔습니다. 자세한 건 함정 절에 적었습니다.

우산 가게 골목 장소 레퍼런스

3. 시트를 레퍼런스로 물려 키프레임을 만듭니다. 클립마다 시작·끝 두 장입니다.

시작(왼쪽)과 끝(오른쪽)이 같은 화각·같은 배경이고 포즈만 다릅니다.

키프레임 시작 - 문 앞에 선 Choco

키프레임 끝 - 빗속으로 한 발 나선 Choco

4. 키프레임 두 장을 앵커로 렌더합니다.

1
2
3
4
./h3 -d ./MiniMax-H3 -p "$PROMPT" \
  --seconds 5 \
  --first-frame kf-start.png --last-frame kf-end.png \
  -o clip1.mp4

앵커 비용은 쌉니다 — 키프레임 2장 처리에 9초 정도 들고, denoise 가 10% 느려지는 정도입니다.

키프레임 두 장만 줘도 중간 동작은 모델이 만들어냅니다. 이전에 만든 영상에서 목도리를 건네는 동작은 프롬프트에만 적었는데 자연스럽게 이어졌습니다.

프롬프트에 소리도 써야 합니다

영상과 오디오가 함께 생성되므로 소리를 안 적으면 소리도 안 나옵니다. subject / action / setting / camera / lighting / audio 를 다 적는 게 좋습니다.

1
2
3
4
5
Rain streams down the glass door of a small umbrella shop. Outside, Choco,
a small round milk-chocolate brown cat with a cream muzzle and chest, stands
soaked in the rain looking in through the wet glass, then the door begins to open.
Soft pastel flat vector animation, clean thick outlines.
Audio: rain on glass, a small brass door bell.

마지막 Audio: 줄이 그 부분입니다.

제가 걸린 함정들

1. 셰이더 경로가 상대경로입니다

h3.c 가 셰이더 파일 경로를 "h3_shaders.metal" 로 하드코딩합니다. 환경변수 오버라이드도 없습니다. 저장소 디렉터리를 현재 위치로 두고 실행해야 합니다.

1
cannot compile h3_shaders.metal: The file couldn't be opened

더 나쁜 건 --info 는 셰이더를 안 건드려서 어디서 실행해도 성공한다는 점입니다. --info 가 잘 되길래 설치가 끝난 줄 알았다가 실제 생성에서 처음 터졌습니다. 설치 확인은 가장 싼 생성 1회로 하세요.

2. 클립 안에서는 카메라가 안 움직입니다

프롬프트에 “카메라가 천천히 물러난다”고 쓰고 동시에 --last-frame 으로 마지막 구도를 못박았더니, 마지막 3.7초 지점에서 컷으로 튀었습니다. 점진적으로 물러나는 게 아니라 한 프레임에 갈아탑니다.

ffmpeg 로 재보면 명확합니다.

1
2
ffmpeg -v error -i out.mp4 -vf "select='gt(scene,0.05)',metadata=print:file=-" -f null -
# 11.333s -> scene_score=0.463   (나머지 전 구간 최댓값 0.125)

--last-frame 은 종료 구도만 만족시키는 제약이고, 모델은 그걸 가장 값싼 방법인 장면 전환으로 풉니다. 그래서 이번 30초는 한 클립 = 한 화각으로 고정하고, 화각 변화는 클립 사이의 컷으로 만들었습니다.

3. 캐릭터 체형을 안 적으면 덩어리가 나옵니다

이미지 프롬프트에 "a small round cat with a tiny body" 정도만 썼더니 팔도 다리도 없이 몸통 아래에 발바닥만 붙은 덩어리가 나왔습니다. 그게 키프레임이었고, 앵커라서 영상이 그걸 충실히 따랐습니다.

팔·다리·꼬리를 대문자로 명시하면 반영됩니다.

1
2
3
He has SHORT VISIBLE ARMS with small rounded paws AND SHORT VISIBLE LEGS
with small feet. He wears a cream canvas apron over his body; the apron
covers the belly but the arms and legs stay clearly visible.

3-1. ★ 네 발로 걷는 캐릭터에게 도구를 들리면 안 됩니다

처음엔 Choco 를 네 발로 서는 보통 고양이로 만들었습니다. 그리고 우산을 들게 했더니 이렇게 나왔습니다.

실패 사례 - 팔이 하나 더 생기고 우산대가 얼굴을 관통

앞다리가 하나 더 생겼고, 우산대가 얼굴을 관통합니다. 네 발로 서 있으려면 앞다리 두 개가 바닥에 있어야 하는데 우산도 쥐어야 하니, 모델이 팔을 하나 더 만들어냈습니다. 쥘 손이 없으니 우산 위치도 어긋납니다.

프롬프트를 고쳐서 해결할 문제가 아니었습니다. 캐릭터 설계 자체가 동작과 모순이었습니다. 두 발로 서는 캐릭터로 바꾸고, 시트에 우산을 쥔 포즈를 직접 포함하니 한 번에 해결됐습니다.

📌 캐릭터가 할 동작을 먼저 정하고, 그 동작이 가능한 몸으로 설계합니다. 그리고 그 동작을 캐릭터 시트에 넣어두면 모델이 추측하지 않습니다.

3-2. 배경에도 레퍼런스가 필요합니다

키프레임을 각각 독립 생성하면서 레퍼런스로 캐릭터 시트만 물렸습니다. 가게에는 레퍼런스가 없었으니 컷마다 다른 건물이 나왔습니다.

같은 클립의 시작·끝 두 장인데 우산 가게가 식물 가게로 바뀌었습니다. 프롬프트에는 양쪽 모두 “동일한 화각, 동일한 가게 위치” 라고 적었습니다.

실패 사례 - 같은 클립의 끝 키프레임이 우산 가게가 아닌 식물 가게로 바뀜

두 가지로 해결했습니다.

장소 레퍼런스를 따로 만들어 키프레임마다 캐릭터 시트와 함께 물립니다. 그리고 “우산 가게”처럼 추상적으로 쓰지 말고 알아볼 수 있는 특징을 나열합니다 — 줄무늬 차양, 창 안의 색색 우산, 문 옆 우산 나무통, 우산 실루엣 간판. 내부도 같은 방식으로 만들어 뒀습니다.

우산 가게 내부 장소 레퍼런스

한 클립의 끝 키프레임은 시작 키프레임을 레퍼런스로 물려 만듭니다. “이 샷의 시작 프레임이다. 화각·배경·조명을 그대로 두고 포즈만 바꿔라” 라고 지시합니다.

이렇게 하면 같은 클립의 시작(위)과 끝(아래)이 어긋나지 않습니다. 비가 조금 더 굵어지고 창 불빛이 밝아진 것만 다릅니다.

수정 후 - 클립 시작 키프레임

수정 후 - 클립 끝 키프레임, 같은 가게 같은 화각

3-3. 키프레임을 먼저 확대해서 봅니다

2시간짜리 렌더를 돌리기 전에 키프레임을 확대해서 보는 게 가장 값싼 검증입니다. 앵커 방식에서는 영상 품질이 키프레임 품질을 넘지 못합니다.

저는 이걸 건너뛰고 12장을 한 번에 만들어 2시간을 렌더한 뒤에야 문제를 발견했습니다. 두 번 만들었습니다.

4. 65W 어댑터로는 완주가 안 됩니다

AC 에 꽂혀 있고 macOS 가 charging 이라고 표시하는데도 배터리가 46% → 9% 로 떨어졌습니다 (58분). M5 Max 가 GPU 를 풀로 쓰면 65W 를 넘게 먹어서, 모자란 만큼을 배터리에서 뺍니다.

상태 배터리
렌더 중 (65W 연결) −0.638 %/분
유휴 충전 +0.836 %/분

충전기를 두 개 꽂아도 소용없습니다. Apple Silicon 은 USB-C 어댑터의 전력을 합치지 않고 하나를 소스로 고릅니다. 실제로 두 개를 연결해도 pmset -g acWattage = 65W 그대로였습니다.

15초 렌더가 두 시간이니 96W 이상이 사실상 요구사항입니다. 사양 이야기할 때 보통 메모리와 디스크를 적는데, 실사용에서 먼저 막히는 건 전원이었습니다.

5. 메모리가 모자라면 denoise 만 7.7배 느려집니다

같은 작업이 어느 날 121.8초, 다른 날 15.7초로 나왔습니다. 전원·저전력 모드·열 경고·바이너리가 전부 정상이라 한참 헤맸습니다.

단계별로 나눠 보니 답이 나왔습니다.

단계 느린 날 정상 배수
텍스트 인코더 8.08초 4.42초 1.8×
DiT 로드 20.56초 15.48초 1.3×
denoise 121.82초 15.74초 7.7×
video VAE 디코더 38.52초 36.81초 1.0×

video VAE 디코더가 양쪽에서 똑같습니다. 가벼운 단계가 아닌데도요. 열이나 전원 문제였다면 이것도 같이 느려졌어야 합니다. denoise 만 다른 점은 19.5GiB 를 상주시킨 채 반복한다는 것이고, 그날 시스템 스왑이 22~23GB, 정상인 날은 2.5GB 였습니다.

h3 자신의 피크(19.5GiB)가 한계(28.1GiB) 안에 넉넉해도 이 일이 생깁니다. 밀려나는 건 시스템 전체의 페이지니까요.

📌 원인을 찾을 때 총 시간만 비교하면 안 됩니다. 단계별 배수를 나란히 놓으면 “균일하게 느림 = 전역 원인” 과 “한 단계만 느림 = 그 단계의 자원” 이 즉시 갈립니다. 안 느려진 단계가 같은 실행 안의 완벽한 대조군 역할을 합니다.

왜 M5 가 그렇게 빠른가

M3 Max 에서 37분 걸리던 게 M5 Max 에서 4분이 됐습니다. 같은 36GB 인데 10.5배입니다.

처음엔 int8 때문이라고 생각했습니다. 그런데 그 비교는 정밀도(BF16→int8) · 가중치 위치(스트리밍→상주) · 하드웨어(M3→M5) 셋이 한꺼번에 바뀐 것이었습니다. M5 에서 M3 와 똑같은 경로(BF16 + --ssd-streaming)로 다시 돌려 하드웨어만 분리했습니다.

실행 denoise 배수 바뀐 것
M3 BF16 스트리밍 2090.5초 기준
M5 BF16 스트리밍 390.2초 5.36× 하드웨어만
M5 int8 상주 199.6초 1.96× int8 + 상주

5.36 × 1.96 = 10.5 로 맞습니다. 하드웨어가 5.4배, int8 이 2.0배입니다.

대역폭 때문이 아닙니다

두 기기의 메모리 대역폭은 300 GB/s → 460 GB/s 로 1.53배입니다. 5.36배를 설명하기엔 한참 모자랍니다.

결정적인 반증이 antirez/h3.c 이슈 #17 에 있습니다. M1 Ultra 는 대역폭이 800 GB/s 로 M5 Max(614)보다 높은데도 denoise 가 5.2배 느립니다. 프로파일에서 encodewait 보다 크니 연산 병목이지 메모리 대기가 아닙니다.

M5 에 행렬 엔진이 새로 들어갔습니다 — 다만 이게 5.36배의 이유는 아닙니다

M1 부터 M4 까지 Apple GPU 에는 전용 행렬 연산 하드웨어가 없었습니다. 모든 행렬 연산이 코어당 128개 범용 ALU 를 그래픽과 같은 파이프라인으로 통과했습니다.

M5(및 A19)에서 GPU 코어마다 Neural Accelerator 가 들어갔습니다. 셰이더 코어 안에 ALU·레이트레이싱과 나란히 놓인 하드웨어 MMA 엔진으로, 개별 GPU 의 텐서 코어에 해당합니다. 독립 벤치마크 실측으로 A19 5코어가 FP16 7.4 TFLOPS / INT8 13.4 TOPS 이고, INT8 이 FP16 의 약 2배입니다.

그런데 이 행렬 엔진이 위 5.36배를 만든 건 아닙니다. 같은 벤치마크가 행렬 유닛이 FP16 과 INT8 만 지원하고 BF16 은 지원하지 않는다고 보고합니다. 5.36배는 양쪽 다 BF16 으로 돌려 얻은 값이라 행렬 엔진을 쓰지 않은 상태의 차이입니다. 즉 그건 클럭·ALU 처리량·캐시 같은 일반 GPU 성능 향상이고, 저는 그 안에서 무엇이 얼마인지까지는 못 갈랐습니다.

행렬 엔진의 몫은 int8 쪽 1.96배이고, 그마저도 “가중치가 메모리에 상주해서 SSD 를 안 읽는” 효과와 섞여 있습니다. 순수 int8 기여는 그보다 작습니다.

📌 정리하면 — M5 가 빠른 이유는 두 겹입니다. 일반 GPU 성능이 크게 올랐고(5.4배), 거기에 int8 행렬 엔진이 얹혔습니다(2.0배). 흔히 “M5 는 AI 가속기 때문에 빠르다”고 말하지만, 적어도 이 워크로드에서는 그게 작은 쪽이었습니다.

📌 로컬 AI 추론용 기기를 고를 때 대역폭 숫자만 보면 오판합니다. 대역폭은 가중치를 읽어오는 속도이고 행렬 엔진은 그걸 곱하는 속도인데, 모델이 메모리에 상주하면 후자가 지배합니다.

int8 이 뭔가요

원래 가중치는 BF16(16비트 부동소수점) 입니다. int88비트 정수(−128~127)라, 그대로 자르면 값이 망가집니다. 그래서 배율(scale)을 따로 저장합니다. h3.c 의 방식은 이렇습니다.

1
2
/* Weights use one F32 scale per output channel;
 * activations are quantized dynamically with one F32 scale per row. */

가중치는 출력 채널마다 F32 배율 하나, 활성값은 행마다 배율 하나를 두고 실행 중에 동적으로 양자화합니다.

  이득 대가
메모리 DiT 36.4GiB → 19.8GiB
처리량 int8 행렬 유닛이 있으면 FP16 의 약 2배 정밀도 손실

품질 손실은 실측 SSIM 차이 0.004 로 이 워크로드에서는 무시할 수준이었습니다.

⚠️ int8 행렬 유닛이 없는 칩에서는 오히려 손해입니다. M4 Pro(M3 와 같은 GPU family 9)에서 게이트를 우회해 측정한 제3자 결과로는 int8 커널이 BF16 보다 1.11~1.12배 느립니다. 곱할 하드웨어가 없으니 양자화가 순수 오버헤드가 됩니다.

Metal 4 는 세대 구분이 아닙니다

h3.c 가 int8 을 켜는 조건을 소스에서 찾아보니 이랬습니다.

1
BOOL m5 = [gpu.device.name rangeOfString:@"M5"].location != NSNotFound;

디바이스 이름에 “M5” 가 있는지만 봅니다. Metal 4 지원 여부도 GPU family 도 조회하지 않습니다.

이상해 보이지만 타당한 판단입니다. 직접 조회해 보니 M1 Ultra · M3 Max · M5 Max 가 전부 MTLGPUFamilyMetal4yes 로 답합니다. 그런데 셰이더에서 mpp::tensor_ops 를 쓰면 M1 Ultra 에서는 컴파일이 실패합니다. Metal 4 조회는 행렬 엔진 유무를 전혀 알려주지 않습니다. 능력 조회로 거를 수단이 없으니 이름으로 거른 겁니다.

게다가 위 이슈의 M4 Pro 검증에 따르면, M3·M4 세대(family 9)에서 게이트를 열면 BF16 이득이 −1.4~+6.2% 로 사실상 없고 int8 은 오히려 1.11~1.12배 느립니다. int8 행렬 유닛이 없어 양자화가 순수 오버헤드가 되기 때문입니다.

만들어 본 30초

우산 가게 주인 모리(두더지)와 비 맞은 고양이 초코 이야기입니다. 5초 × 6컷으로 나눠 찍고 이어붙였습니다.

# 화각 내용
1 와이드 비 오는 골목, 작은 우산 가게
2 미디엄 모리가 카운터에서 우산을 손질한다
3 클로즈업 유리문 밖에 젖은 초코가 서 있다
4 미디엄 모리가 노란 우산을 건넨다
5 미디엄 초코가 우산을 쓰고 빗속으로 나선다
6 와이드 노란 우산 하나가 회색 골목을 걸어간다

한 클립 = 한 화각으로 고정하고 화각 변화는 컷으로 만들었습니다. 앞서 적은 대로 클립 안에서는 카메라가 안 움직이기 때문입니다.

정리

  • 된다. 36GB 맥북에서 100GB 넘는 모델이 돕니다. 인코더를 레이어 단위로 스트리밍하고, 단계별로 로드·해제하고, VAE 를 타일링하기 때문입니다
  • 길게 만들려면 잘게 나눠 찍는다. 지수 1.9 라 한 번에 길게 뽑는 게 가장 비쌉니다
  • 키프레임이 결과를 결정한다. 영상이 키프레임 품질을 못 넘습니다
  • 전원을 확인한다. 65W 로는 못 버팁니다
  • 라이선스를 확인한다. 한국은 제외 지역이라 개별 인가가 필요합니다