안녕하세요, log 쌓는 MK 입니다.
지난 편에서 갤럭시 S24+ 모델을 기준으로 ACE-Step 1.5 모델을 기반으로 음악 생성을 진행했는데요.
실제 사용된 모델은 다음과 같습니다.
| 모델 | 대략적인 크기 | 역할 |
|---|---|---|
| acestep-5Hz-lm-1.7B-Q8_0.gguf | 약 2GB | 텍스트와 가사를 음악용 semantic audio code로 변환 |
| Qwen3-Embedding-0.6B-Q8_0.gguf | 약 784MB | 프롬프트와 가사를 DiT conditioning 벡터로 인코딩 |
| acestep-v15-turbo-Q4_K_M.gguf | 약 1.45GB | audio code를 음악 잠재 벡터로 합성 |
| vae-BF16.gguf | 약 337MB | 잠재 벡터를 48kHz 스테레오 오디오 파형으로 복원 |
LM의 경우 0.6B로도 테스트를 했었지만 audio code가 생성되지 않거나 후반부에 무너지는 경우가 많아 느리더라도 1.7B 모델을 사용할 수 밖에 없었습니다.
그리고 사이즈 상 모델은 APK에 포함하지 않고, 별도로 기기에 저장하였습니다.
전체 아키텍처
Android UI
Kotlin Activity + ViewModel
│
▼
ModelBridge
JNI 함수 선언
│
▼
native-lib.cpp
모델 수명·추론·취소·PCM 변환
│
▼
acestep.cpp + GGML
LM → Text Encoder → DiT → VAE
│
▼
48kHz Stereo Float PCM
│
▼
WAV 저장 + AudioTrack 재생
1단계: LM으로 음악 기획
처음 의도는 사용자가 프롬프트를 입력하는 형태로 했으나 변수가 많이 발생해 기본 정보를 모두 고정한 상태에서 진행했습니다.
실제로 실험 중 생성에 실패한 오디오를 분석해 보면 LM 단계에서 이미 무너진 경우가 많았는데 실패 형태는 다음과 같은 형태였습니다.
..., 35847, 35847, 35847, 35847, 35847, ...
후반부에서 특정 code가 반복되면서 보컬이 초반에만 재생되거나, 같은 소리가 반복되거나, 하울링처럼 들리는 현상이 빈번했습니다.
이를 위해 기본 정보를 고정한 것 뿐만 아니라 LM 결과에 대해서 다음 항목을 검사했습니다.
- 생성된 code 개수가 목표 길이에 맞는가
- 전체 code의 고유값 비율이 충분한가
- 같은 code가 과도하게 연속되는가
- 후반 40개 code가 특정 값에 편중되는가
- 40-code sliding window 안에서 다양성이 유지되는가
- 짧은 패턴이 주기적으로 반복되는가
품질 검사가 실패하면 다음 단계로 넘어가지 않고 LM 단계를 최대 3회까지 다시 실행했습니다.
이렇게 LM 단계를 통과하면 대부분 음악 같은 음악(?)이 나왔습니다.
2단계: DiT와 VAE로 실제 음악을 만든다
LM 품질 검사를 통과하면 결과 JSON 전체를 Synth 화면으로 전달하는데요.
Synth 단계는 크게 두 부분으로 나뉩니다.
프롬프트·가사·audio codes
│
▼
Text Encoder + Conditioning
│
▼
DiT diffusion
│
▼
음악 latent
│
▼
VAE decode
│
▼
48kHz stereo PCM
Turbo DiT의 기본 inference step은 8입니다.
앱에서는 실험을 위해 사용자가 step 수를 직접 입력할 수도 있었지만 현재 Turbo 모델에는 8 step을 기본값으로 사용합니다.
step 수를 줄이면 생성 속도는 빨라지지만 품질이 떨어질 수 있고, 반대로 step 수를 높인다고 모바일 환경에서 항상 품질이 비례해 개선되는 것은 아니었습니다.
모바일 Vulkan에서 발생한 문제
가장 골치 아팠던 부분입니다. GGML Vulkan 백엔드를 활성화했다고 해서 모든 연산을 그대로 GPU에 올릴 수 있는 것은 아니었습니다.
특히 Galaxy S24+의 Xclipse GPU에서는 수치 안정성과 메모리 사용량을 함께 고려해야 했습니다. 제가 최종적으로 사용한 Synth 설정은 다음과 같습니다.
- Vulkan 백엔드 활성화
- Flash Attention 비활성화
- FP16 값 clamp 활성화
- Batch CFG 비활성화
- VAE chunk 128
- VAE overlap 32
저도 모든 걸 다 이해하고 진행한 것은 아니지만 Codex와 수없이 테스트 하며 그래도 온전히 동작하는 값을 찾은 것으로 봐주시면 될 것 같습니다.
CPU 성능 코어 활용
Android 기기는 일반적으로 저전력 코어와 고성능 코어가 섞여 있고, 무거운 LM 추론 스레드가 저전력 코어에 배치되면 생성 시간이 크게 늘어날 수 있습니다. 실제로 이 문제로 폰이 뻗은 적이 한 두번이 아닙니다.
다음 시스템 정보를 런타임에 읽어서 가장 높은 최대 주파수를 기준으로 성능 코어 클러스터를 선택하고, LM 로딩과 생성 스레드의 CPU affinity를 해당 코어에 고정했습니다.
실제 구현을 통해 얻은 결론
-
음악 품질은 LM 단계에서 상당 부분 결정된다 처음에는 DiT나 VAE, Vulkan 구현을 주요 원인으로 의심했습니다. 하지만 반복 실험 결과, 실패한 음악은 대부분 LM의 audio code가 이미 붕괴해 있었다는 걸 뒤늦게 발견했습니다.
Synth 성능을 최적화하기 전에 LM 출력 품질을 확인하는 것이 더 중요해 보였습니다. -
작은 모델이 항상 실용적인 것은 아니다
0.6B LM은 빠르지만 audio code 생성 안정성이 부족했습니다. 모바일에서는 속도가 중요하지만, 빠르게 실패한 결과를 만드는 것은 의미가 없었습니다.
최종적으로 1.7B Q8 모델과 품질 검사·자동 재시도의 조합을 사용해 결과를 도출했습니다. -
모바일 GPU 최적화는 데스크톱 설정을 그대로 복사하는 일이 아니다
Flash Attention, FP16, batch 처리처럼 일반적으로 빠르다고 알려진 설정도 모바일 GPU에서는 잘못된 출력이나 OOM을 유발할 수 있었습니다.
기능을 활성화했는지보다 실제 기기에서 정상적인 오디오가 나오는지가 더 중요했습니다. -
메모리 절약과 재시도 전략이 필요하다
VAE chunk를 작게 나누고, 실패하면 더 작은 타일로 재시도하도록 만들었습니다. 하나의 최적값에 의존하기보다 실패 시 안전한 설정으로 내려가는 전략이 모바일 환경에 더 적합했습니다.
마치며
안드로이드 기기에서 ACE-Step 1.5를 이용해 온 디바이스로 음악을 생성하는 작업은 간단한 음악까지는 생성 가능했지만 실제 사용 가능한 수준은 아니라고 판단했습니다. 이유는 너무 오랜 생성 시간이 필요하고 하드웨어 최소 사양이 너무 높기 때문입니다.
지금 제게 갤럭시 S26 울트라가 있기 때문에 좀 더 나은 성능을 보여줄 수 있겠지만 이 역시 한계가 있음을 알기에 추가 시도는 해보지 않았습니다.
그래서 온디바이스 실험은 여기서 마치기로 했습니다. 꽤나 즐거운 시간이었고, 그래도 어설프지만 결과물이 나왔다는 것에 만족합니다.
생성된 결과물은 제 스레드에서 확인하실 수 있습니다.
감사합니다.