1177 lines
47 KiB
Markdown
1177 lines
47 KiB
Markdown
# Architecture
|
|
|
|
> Full technical reference for acestep.cpp. For a quick start guide, see [README.md](../README.md).
|
|
|
|
# acestep.cpp
|
|
|
|
Portable C++17 implementation of ACE-Step 1.5 music generation using GGML.
|
|
Text + lyrics in, stereo 48kHz MP3 or WAV out. Runs on CPU, CUDA, ROCm, Metal, Vulkan.
|
|
|
|
## Build
|
|
|
|
```bash
|
|
git submodule update --init
|
|
|
|
mkdir build && cd build
|
|
|
|
# macOS (Metal + Accelerate BLAS auto-enabled)
|
|
cmake ..
|
|
|
|
# Linux with NVIDIA GPU
|
|
cmake .. -DGGML_CUDA=ON
|
|
|
|
# Linux with AMD GPU (ROCm)
|
|
cmake .. -DGGML_HIP=ON
|
|
|
|
# Linux with Vulkan
|
|
cmake .. -DGGML_VULKAN=ON
|
|
|
|
cmake --build . --config Release -j$(nproc)
|
|
```
|
|
|
|
### Windows
|
|
|
|
Install [Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
|
|
(select "Desktop development with C++" workload) and optionally the
|
|
[CUDA Toolkit](https://developer.nvidia.com/cuda-downloads) and/or the
|
|
[Vulkan SDK](https://vulkan.lunarg.com/sdk/home).
|
|
|
|
```cmd
|
|
git submodule update --init
|
|
|
|
call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
|
|
|
mkdir build
|
|
cd build
|
|
|
|
rem NVIDIA GPU
|
|
cmake .. -DGGML_CUDA=ON
|
|
|
|
rem AMD/Intel GPU (Vulkan)
|
|
cmake .. -DGGML_VULKAN=ON
|
|
|
|
rem all backends (CUDA + Vulkan + CPU, runtime loading)
|
|
cmake .. -DGGML_CPU_ALL_VARIANTS=ON -DGGML_CUDA=ON -DGGML_VULKAN=ON -DGGML_BACKEND_DL=ON
|
|
|
|
cmake --build . --config Release -j %NUMBER_OF_PROCESSORS%
|
|
```
|
|
|
|
Builds seven binaries: `ace-lm` (LLM), `ace-synth` (DiT + VAE), `ace-server` (HTTP server), `ace-understand` (reverse: audio -> metadata), `neural-codec` (VAE encode/decode), `mp3-codec` (MP3 encoder/decoder) and `quantize` (GGUF requantizer).
|
|
|
|
## Models
|
|
|
|
Pre-quantized GGUFs on [Hugging Face](https://huggingface.co/Serveurperso/ACE-Step-1.5-GGUF).
|
|
|
|
```bash
|
|
pip install hf
|
|
./models.sh # Q8_0 turbo essentials (~7.7 GB)
|
|
./models.sh --all # every model, every quant (~97 GB)
|
|
./models.sh --quant Q6_K # pick a specific quant (Q4_K_M, Q5_K_M, Q6_K, Q8_0, BF16)
|
|
./models.sh --sft # add SFT DiT variant
|
|
./models.sh --shifts # add shift1/shift3/continuous variants
|
|
```
|
|
|
|
Default downloads 4 files into `models/`:
|
|
|
|
| GGUF | Arch | Size |
|
|
|------|------|------|
|
|
| Qwen3-Embedding-0.6B-Q8_0.gguf | text encoder (28L, H=1024) | 748 MB |
|
|
| acestep-5Hz-lm-4B-Q8_0.gguf | Qwen3 causal LM | 4.2 GB |
|
|
| acestep-v15-turbo-Q8_0.gguf | DiT 2B + CondEncoder (24L, H=2048) | 2.4 GB |
|
|
| vae-BF16.gguf | AutoencoderOobleck | 322 MB |
|
|
|
|
Three LM sizes: 0.6B (fast), 1.7B, 4B (best quality).
|
|
Six DiT variants: turbo, sft, base, turbo-shift1, turbo-shift3, turbo-continuous.
|
|
XL (4B DiT) variants: xl-turbo, xl-sft, xl-base (32L, H=2560, higher quality, ~9.5 GB BF16).
|
|
VAE is always BF16 (small, bandwidth-bound, quality-critical).
|
|
|
|
<details>
|
|
<summary>Building GGUFs from source (checkpoints + convert)</summary>
|
|
|
|
If you want to convert from the original safetensors yourself:
|
|
|
|
```bash
|
|
pip install gguf hf
|
|
./checkpoints.sh # download raw HF checkpoints (turbo + 4B LM)
|
|
./checkpoints.sh --all # all variants (SFT, shift1/3, 0.6B/1.7B LM)
|
|
python3 convert.py # convert all checkpoints to GGUF (models/)
|
|
./quantize.sh # quantize BF16 -> Q4_K_M/Q5_K_M/Q6_K/Q8_0
|
|
```
|
|
|
|
`checkpoints.sh` downloads safetensors, config.json, and tokenizer files
|
|
into `checkpoints/`. `convert.py` packs everything into self-contained
|
|
GGUF files in `models/`, bundling BPE tokenizer, silence_latent, and
|
|
config metadata so no external file is needed at runtime.
|
|
|
|
</details>
|
|
|
|
## CLI
|
|
|
|
`ace-lm` generates lyrics and audio codes, `ace-synth` synthesizes audio.
|
|
The input JSON is never modified. Output is always numbered: `request0.json`.
|
|
|
|
```bash
|
|
cat > /tmp/request.json << 'EOF'
|
|
{
|
|
"caption": "Upbeat pop rock with driving guitars and catchy hooks",
|
|
"inference_steps": 8,
|
|
"shift": 3.0,
|
|
"vocal_language": "fr"
|
|
}
|
|
EOF
|
|
|
|
# LLM: request.json -> request0.json (enriched with metadata + lyrics + codes)
|
|
./ace-lm \
|
|
--request /tmp/request.json \
|
|
--lm models/acestep-5Hz-lm-4B-Q8_0.gguf
|
|
|
|
# DiT+VAE: request0.json -> request00.mp3
|
|
./ace-synth \
|
|
--request /tmp/request0.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-turbo-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf
|
|
```
|
|
|
|
With an adapter (LoRA today, PEFT directory or ComfyUI single file):
|
|
|
|
```bash
|
|
# PEFT directory (contains adapter_model.safetensors + adapter_config.json)
|
|
./ace-synth \
|
|
--request /tmp/request0.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-turbo-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf \
|
|
--adapter /path/to/peft-adapter
|
|
|
|
# ComfyUI single .safetensors file (alpha baked in, no config needed)
|
|
./ace-synth \
|
|
--request /tmp/request0.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-turbo-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf \
|
|
--adapter best_sft_v2_2338_comfyui.safetensors
|
|
```
|
|
|
|
Generate multiple songs at once with `lm_batch_size` in the JSON:
|
|
|
|
```bash
|
|
# 2 different songs from one prompt (different lyrics, codes, metadata)
|
|
cat > /tmp/request.json << 'EOF'
|
|
{
|
|
"caption": "Upbeat pop rock anthem with driving guitars and catchy hooks",
|
|
"vocal_language": "fr",
|
|
"lm_batch_size": 2
|
|
}
|
|
EOF
|
|
|
|
# LM: request.json (lm_batch_size=2) -> request0.json, request1.json
|
|
./ace-lm \
|
|
--request /tmp/request.json \
|
|
--lm models/acestep-5Hz-lm-4B-Q8_0.gguf
|
|
|
|
# DiT+VAE: both requests in one GPU batch -> request00.mp3, request10.mp3
|
|
./ace-synth \
|
|
--request /tmp/request0.json /tmp/request1.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-turbo-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf
|
|
```
|
|
|
|
`lm_batch_size` controls how many songs the LM generates. User-provided
|
|
fields are preserved in all outputs. Empty fields are filled independently
|
|
per batch item, producing genuinely different songs.
|
|
ace-synth takes all request files as CLI arguments and runs them in a
|
|
single GPU batch.
|
|
|
|
Transform an existing song with `--src-audio` (no LLM needed):
|
|
|
|
```bash
|
|
cat > /tmp/cover.json << 'EOF'
|
|
{
|
|
"task_type": "cover",
|
|
"caption": "Jazz piano cover with brushed drums and walking bass",
|
|
"lyrics": "[Instrumental]"
|
|
}
|
|
EOF
|
|
|
|
./ace-synth \
|
|
--src-audio song.wav \
|
|
--request /tmp/cover.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-turbo-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf \
|
|
```
|
|
|
|
Ready-made examples in `examples/`:
|
|
|
|
```bash
|
|
cd examples
|
|
./simple.sh # caption only, LLM fills everything
|
|
./simple-batch.sh # 2 songs from one prompt (lm_batch_size=2)
|
|
./partial.sh # caption + lyrics + duration
|
|
./full.sh # all metadata provided
|
|
./dit-only.sh # skip LLM, DiT from noise
|
|
./server-turbo.sh # start HTTP server (turbo model)
|
|
./server-sft.sh # start HTTP server (SFT model)
|
|
./client.sh # test server (single song)
|
|
./client-batch.py # test server batch (2 songs)
|
|
./client-understand.sh <audio> # test /understand + /synth roundtrip
|
|
```
|
|
|
|
Each example has a `-sft` variant (SFT model, 50 steps, CFG 1.0)
|
|
alongside the turbo default (8 steps, no CFG).
|
|
|
|
## Generation modes
|
|
|
|
The LLM fills what's missing in the JSON and generates audio codes.
|
|
Empty field = "fill it". Filled = "don't touch".
|
|
All modes always output numbered files (`request0.json` .. `requestN-1.json`).
|
|
The input JSON is never modified.
|
|
|
|
**Caption only** (`lyrics=""`): two LLM passes. Phase 1 uses the "Expand"
|
|
prompt to generate an enriched caption, lyrics, and metadata (bpm, keyscale,
|
|
timesignature, duration, vocal_language) via CoT. Phase 2 reinjects the CoT
|
|
and generates audio codes using the "Generate tokens" prompt. CFG is forced
|
|
to 1.0 in phase 1 (free sampling); `lm_cfg_scale` only applies in phase 2.
|
|
With `lm_batch_size > 1`, each element runs its own phase 1,
|
|
producing N completely different songs. See `examples/simple-batch.json`.
|
|
|
|
**Caption + lyrics (+ optional metadata)**: single LLM pass. The "Generate
|
|
tokens" prompt is used directly. Missing metadata is filled via CoT, the
|
|
caption is enriched, and audio codes are generated. User-provided metadata
|
|
fields are never overwritten. `lm_cfg_scale` applies to both CoT and code
|
|
generation. See `examples/partial.json`.
|
|
|
|
**Everything provided** (caption, lyrics, bpm, duration, keyscale,
|
|
timesignature): the LLM skips CoT and generates audio codes directly.
|
|
With `lm_batch_size > 1`, all elements share the same prompt (single prefill,
|
|
KV cache copied), producing N different audio code sets. See `examples/full.json`.
|
|
|
|
**Instrumental** (`lyrics="[Instrumental]"`): treated as "lyrics provided",
|
|
so the single-pass "Generate tokens" path is used. No lyrics generation.
|
|
The DiT was trained with this exact string as the no-vocal condition.
|
|
|
|
**Passthrough** (`audio_codes` present): LLM is skipped entirely.
|
|
Run `ace-synth` to decode existing codes. See `examples/dit-only.json`.
|
|
|
|
**Cover** (`"task_type": "cover"` + `--src-audio`): no LLM needed. The source audio
|
|
(WAV or MP3, any sample rate) is resampled to 48kHz, VAE-encoded to latent
|
|
space, then passed through an FSQ roundtrip (tokenize 25Hz to 5Hz, detokenize
|
|
back to 25Hz). The lossy 5:1 temporal compression destroys micro-timings,
|
|
ornaments and transients, so the DiT diverges from the source and produces
|
|
a free reinterpretation rather than a close remix.
|
|
`audio_cover_strength` in the JSON controls how many DiT steps see the source
|
|
(0.5 = half the steps use source context, half use silence). The caption
|
|
steers the style while the source provides loose structure.
|
|
Duration is determined by the source audio.
|
|
|
|
**Cover-nofsq** (`"task_type": "cover-nofsq"` + `--src-audio`): cover variant
|
|
that skips the FSQ roundtrip. The DiT receives clean VAE latents at 25Hz,
|
|
preserving the full detail of the source. Produces remixes that stay close
|
|
to the original structure, melody, and timbre. Pass `--ref-audio` pointing to
|
|
the same file as `--src-audio` for best results.
|
|
`audio_cover_strength` works well at higher values (0.2 to 0.5) compared to
|
|
regular cover. Same JSON fields as cover, just change the task_type.
|
|
|
|
**Repaint** (`"task_type": "repaint"` + `--src-audio`):
|
|
regenerates a time region of the source audio while preserving the rest.
|
|
`repainting_start` and `repainting_end` define the region in seconds.
|
|
Default start is 0. Default end (`-1`) resolves to source start when
|
|
outpainting (start < 0) or source duration otherwise.
|
|
Negative start outpaints before the source, end beyond source duration
|
|
outpaints after. The source audio is padded with silence before VAE
|
|
encoding. `audio_cover_strength` is ignored (the mask handles everything).
|
|
|
|
```bash
|
|
# Inpaint: regenerate seconds 10-25
|
|
cat > /tmp/repaint.json << 'EOF'
|
|
{
|
|
"task_type": "repaint",
|
|
"caption": "Smooth jazz guitar solo with reverb",
|
|
"lyrics": "[Instrumental]",
|
|
"repainting_start": 10.0,
|
|
"repainting_end": 25.0,
|
|
"inference_steps": 50,
|
|
"guidance_scale": 1.0,
|
|
"shift": 1.0
|
|
}
|
|
EOF
|
|
|
|
# Outpaint: generate 5s before the song (end defaults to 0)
|
|
cat > /tmp/outpaint.json << 'EOF'
|
|
{
|
|
"task_type": "repaint",
|
|
"caption": "Smooth jazz intro building into the main theme",
|
|
"lyrics": "[Instrumental]",
|
|
"repainting_start": -5.0,
|
|
"inference_steps": 50,
|
|
"guidance_scale": 1.0,
|
|
"shift": 1.0
|
|
}
|
|
EOF
|
|
|
|
./ace-synth \
|
|
--src-audio song.wav \
|
|
--request /tmp/repaint.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-sft-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf
|
|
```
|
|
|
|
**Lego** (`"task_type": "lego"` + `--src-audio`):
|
|
generates a new instrument track layered over an existing backing track.
|
|
See `examples/lego.json` and `examples/lego.sh`.
|
|
|
|
```bash
|
|
cat > /tmp/lego.json << 'EOF'
|
|
{
|
|
"caption": "electric guitar riff, funk guitar, house music, instrumental",
|
|
"lyrics": "[Instrumental]",
|
|
"task_type": "lego",
|
|
"track": "guitar",
|
|
"inference_steps": 50,
|
|
"guidance_scale": 1.0,
|
|
"shift": 1.0
|
|
}
|
|
EOF
|
|
|
|
./ace-synth \
|
|
--src-audio backing-track.wav \
|
|
--request /tmp/lego.json \
|
|
--embedding models/Qwen3-Embedding-0.6B-Q8_0.gguf \
|
|
--dit models/acestep-v15-base-Q8_0.gguf \
|
|
--vae models/vae-BF16.gguf \
|
|
--wav
|
|
```
|
|
|
|
Available track names for lego, extract, and complete: `vocals`, `backing_vocals`,
|
|
`drums`, `bass`, `guitar`, `keyboard`, `percussion`, `strings`, `synth`, `fx`,
|
|
`brass`, `woodwinds`.
|
|
|
|
### Model compatibility
|
|
|
|
| Task | Turbo | Base/SFT | LM used |
|
|
|------|-------|----------|---------|
|
|
| text2music | yes | yes | yes |
|
|
| cover | yes | yes | no (skipped) |
|
|
| cover-nofsq | yes | yes | no (skipped) |
|
|
| repaint | yes | yes | no (skipped) |
|
|
| lego | no | yes | yes |
|
|
| extract | no | yes | no (skipped) |
|
|
| complete | no | yes | yes |
|
|
|
|
For skipped tasks, `caption` and `lyrics` are passed verbatim to the DiT.
|
|
|
|
### DiT context per mode
|
|
|
|
What the DiT actually receives in its 128-channel context `[src(64) | mask(64)]`:
|
|
|
|
| Mode | src channels | mask value | instruction |
|
|
|------|-------------|------------|-------------|
|
|
| text2music | silence | 1.0 | "Fill the audio semantic mask..." |
|
|
| cover | FSQ(src) roundtrip | 1.0 | "Generate audio semantic tokens..." |
|
|
| cover-nofsq | raw VAE src (no FSQ) | 1.0 | "Generate audio semantic tokens..." |
|
|
| repaint | silence in zone / src outside | 0.0 outside / 1.0 in zone | "Repaint the mask area..." |
|
|
| lego (no region) | raw VAE src everywhere | 1.0 | "Generate the TRACK track..." |
|
|
| lego (with region) | raw VAE src everywhere | 0.0 outside / 1.0 in zone | "Generate the TRACK track..." |
|
|
| extract | raw VAE src | 1.0 | "Extract the TRACK track..." |
|
|
| complete | raw VAE src | 1.0 | "Complete the input track..." |
|
|
|
|
cover uses an FSQ roundtrip (tokenize 25Hz->5Hz then detokenize 5Hz->25Hz). The
|
|
lossy compression destroys source detail and the DiT diverges freely.
|
|
cover-nofsq skips this roundtrip: same instruction, clean 25Hz latents. The DiT
|
|
stays close to the source and produces faithful remixes. Pass
|
|
ref_audio = src_audio for best results.
|
|
All other tasks with source audio use raw VAE latents (no FSQ).
|
|
|
|
### Region-mode pipeline (repaint + lego with region)
|
|
|
|
Region coordinates are resolved in a unified block after mode routing:
|
|
`s.rs += left_pad_sec; s.re += left_pad_sec`. When outpainting is active,
|
|
source audio has been padded with silence before VAE encoding, so T_cover
|
|
and all downstream latent operations naturally reflect the extended canvas.
|
|
|
|
Three mechanisms stack on top of each other when a repaint region is active:
|
|
|
|
1. **Step injection** (denoising loop, first 50% of steps): frames outside the
|
|
region are forced back to `t_next * noise + (1 - t_next) * src_latents` at each
|
|
step. This prevents the DiT from drifting outside the preserved zone.
|
|
For repaint: src_latents = full source (prevents decay of preserved content).
|
|
For lego: same formula, src = full backing track.
|
|
|
|
2. **Latent boundary blend** (post-generation, pre-VAE): 12-frame linear crossfade
|
|
at region edges. Outside-zone latents blend from generated toward source.
|
|
Formula: `output[t] = m * generated[t] + (1-m) * src[t]`
|
|
where m ramps 0->1 approaching the zone boundary.
|
|
|
|
3. **Waveform splice** (post-VAE decode): replaces non-region audio samples with
|
|
the original PCM from `src_audio` (interleaved input), with a 25ms linear
|
|
crossfade at zone edges. Eliminates VAE reconstruction artifacts in preserved
|
|
regions. Skipped if region covers the full duration.
|
|
|
|
Key difference repaint vs lego: repaint silences the zone in the DiT context src
|
|
(so the DiT generates fresh content there). Lego keeps the full backing track in
|
|
context even inside the zone (DiT generates a new layer that harmonizes with it).
|
|
|
|
### CLI scope
|
|
|
|
`ace-synth` and `ace-lm` expose cover and repaint via `--src-audio` with all
|
|
model types. Lego, extract, and complete are accessible via JSON request
|
|
(`task_type` field) and the HTTP server, but have no dedicated CLI flag:
|
|
pass `--src-audio` and set `task_type` in the JSON directly. These three modes
|
|
require a base or SFT model (not turbo).
|
|
|
|
|
|
## Request JSON reference
|
|
|
|
Only `caption` is required. All other fields default to "unset" which means
|
|
the LLM fills them, or a sensible runtime default is applied.
|
|
|
|
```json
|
|
{
|
|
"caption": "",
|
|
"lyrics": "",
|
|
"bpm": 0,
|
|
"duration": 0,
|
|
"keyscale": "",
|
|
"timesignature": "",
|
|
"vocal_language": "",
|
|
"seed": -1,
|
|
"lm_batch_size": 1,
|
|
"synth_batch_size": 1,
|
|
"lm_temperature": 0.85,
|
|
"lm_cfg_scale": 2.0,
|
|
"lm_top_p": 0.9,
|
|
"lm_top_k": 0,
|
|
"lm_negative_prompt": "",
|
|
"use_cot_caption": true,
|
|
"audio_codes": "",
|
|
"inference_steps": 0,
|
|
"guidance_scale": 0.0,
|
|
"shift": 0.0,
|
|
"audio_cover_strength": 1.0,
|
|
"cover_noise_strength": 0.0,
|
|
"repainting_start": 0,
|
|
"repainting_end": -1,
|
|
"task_type": "",
|
|
"track": "",
|
|
"infer_method": "",
|
|
"synth_model": "",
|
|
"lm_model": "",
|
|
"adapter": "",
|
|
"adapter_scale": 1.0
|
|
}
|
|
```
|
|
|
|
### Text conditioning (ace-lm + ace-synth)
|
|
|
|
**`caption`** (string, required)
|
|
Natural language description of the music style, mood, instruments, etc.
|
|
Fed to both the LLM and the DiT text encoder.
|
|
|
|
**`lyrics`** (string, default `""`)
|
|
Controls vocal generation. Three valid states:
|
|
- `""`: LLM generates lyrics from the caption (phase 1 "Expand" prompt).
|
|
- `"[Instrumental]"`: no vocals. Passed directly to the DiT, LLM skips lyrics generation.
|
|
- Any other string: user-provided lyrics used as-is, LLM only fills missing metadata.
|
|
|
|
There is no `instrumental` flag. This field is the single source of truth for
|
|
vocal content.
|
|
|
|
### Metadata (LLM-filled if unset)
|
|
|
|
**`bpm`** (int, default `0` = unset)
|
|
Beats per minute. LLM generates one if 0.
|
|
|
|
**`duration`** (float seconds, default `0` = unset)
|
|
Target audio duration. `0` means the LLM picks it. FSM constrains LLM output
|
|
to [10, 600]s; values <= 0 after generation fall back to 120s.
|
|
|
|
**`keyscale`** (string, default `""` = unset)
|
|
Musical key and scale, e.g. `"C major"`, `"F# minor"`. LLM fills if empty.
|
|
|
|
**`timesignature`** (string, default `""` = unset)
|
|
Time signature numerator as a string, e.g. `"4"` for 4/4, `"3"` for 3/4.
|
|
LLM fills if empty.
|
|
|
|
**`vocal_language`** (string, default `""` = unset)
|
|
BCP-47 language code for lyrics, e.g. `"en"`, `"fr"`, `"ja"`. Three states:
|
|
- `""`: LLM detects the language via CoT and fills this field.
|
|
- `"unknown"`: explicit "no specific language" signal to the DiT.
|
|
- Any language code: used as-is. When lyrics are being generated, the FSM
|
|
constrains the LLM output to that language.
|
|
|
|
### Generation control
|
|
|
|
**`seed`** (int64, default `-1` = random)
|
|
RNG seed for the DiT pipeline (Philox noise). The LM always uses a
|
|
random seed internally.
|
|
|
|
**`lm_batch_size`** (int, default `1`)
|
|
Number of LM variations. Has no effect on ace-synth.
|
|
|
|
**`synth_batch_size`** (int, default `1`)
|
|
Number of DiT variations per request. Works in all modes: text2music,
|
|
cover, repaint, lego, extract, complete. Combined with `lm_batch_size`, you get
|
|
`lm_batch_size * synth_batch_size` total outputs.
|
|
|
|
### Batching
|
|
|
|
Three rules govern all batching, in both CLIs and the server:
|
|
|
|
1. Each input JSON is executed independently, as if it were the only one.
|
|
2. `seed=-1` is resolved to a random value once per input JSON.
|
|
An explicit seed is used as-is.
|
|
3. `lm_batch_size=N` duplicates with consecutive LM-internal seeds.
|
|
`synth_batch_size=N` duplicates with consecutive `seed` values.
|
|
|
|
**`audio_codes`** (string, default `""`)
|
|
Comma-separated FSQ token IDs produced by ace-lm. When non-empty, the
|
|
entire LLM pass is skipped and ace-synth decodes these codes directly
|
|
(passthrough mode).
|
|
|
|
**`audio_cover_strength`** (float, default `1.0`)
|
|
Only used in `cover` mode. Fraction of DiT steps that see the source audio
|
|
as context. At `1.0` all steps use the source. At `0.0` no
|
|
steps use the source (pure text2music, source is ignored). Values below 1.0
|
|
switch DiT context to silence and encoder hidden states to text2music
|
|
instruction at the corresponding step. Lower values give more creative
|
|
freedom, higher values preserve more of the original structure.
|
|
Defaults to 1.0 for `lego`, `extract`, `complete` (context-switch inactive for these modes).
|
|
Ignored in `repaint` mode (the mask handles everything).
|
|
|
|
**`cover_noise_strength`** (float, default `0.0`)
|
|
Only used in `cover` mode. Blends initial noise with source latents before
|
|
diffusion starts. `0.0` = pure noise (default). `1.0` = start nearly identical
|
|
to the source. The schedule is truncated to the nearest timestep matching the
|
|
noise level. `cover_steps` is recalculated against the remaining steps.
|
|
|
|
**`repainting_start`** (float seconds, default `0`)
|
|
**`repainting_end`** (float seconds, default `-1`)
|
|
Region boundaries for `repaint` and `lego` modes. Default end (`-1`) resolves
|
|
to source start when outpainting (start < 0), source duration otherwise.
|
|
Negative start pads silence before, end beyond source duration pads after.
|
|
Error if end <= start after adjustment.
|
|
|
|
**`task_type`** (string, default `""` = `text2music`)
|
|
Controls the generation mode. This field is the single source of truth for
|
|
what the pipeline does. Empty is equivalent to `text2music`.
|
|
Values: `text2music`, `cover`, `cover-nofsq`, `repaint`, `lego`, `extract`, `complete`.
|
|
|
|
- `text2music`: standard text-to-music synthesis from silence.
|
|
- `cover`: re-synthesize source audio with a new style. FSQ roundtrip degrades
|
|
source latents, so the DiT diverges freely. Requires `--src-audio`.
|
|
`audio_cover_strength` controls how many DiT steps see the source.
|
|
- `cover-nofsq`: remix source audio without FSQ roundtrip. The DiT works on
|
|
clean 25Hz VAE latents and stays close to the original. Requires `--src-audio`.
|
|
Pass `--ref-audio` = `--src-audio` for best results.
|
|
- `repaint`: regenerate a time region of the source audio. Requires `--src-audio`.
|
|
Negative start outpaints before, end beyond duration outpaints after.
|
|
- `lego`: generate a new instrument track in context of a backing track. Requires
|
|
`--src-audio` and `track`. Base model only. Output is the generated track
|
|
(behavior analogous to stem generation; the output mix vs isolated stem is
|
|
model-dependent and unverified in this codebase).
|
|
Supports optional region constraint via `repainting_start/end`.
|
|
- `extract`: isolate a specific stem from a mixed source. Requires `--src-audio`
|
|
and `track`. Base model only. LM is skipped (same as cover/repaint).
|
|
- `complete`: generate a full mix from a single isolated stem. Requires `--src-audio`
|
|
(the isolated stem, e.g. a cappella vocals) and `track` (what to add, e.g. `drums`).
|
|
Base model only. Output duration = source duration. The DiT regenerates all frames
|
|
conditioned on the stem; it does NOT splice or extend temporally.
|
|
`track` can be a pre-formatted string like `"VOCALS | DRUMS"` for multi-stem.
|
|
|
|
`lego`, `extract`, and `complete` always use the full source context
|
|
(`audio_cover_strength` defaults to 1.0 and the context-switch mechanism is inactive).
|
|
|
|
**`track`** (string, default `""`)
|
|
Track name for `lego`, `extract`, and `complete` modes. Standard names: `vocals`, `backing_vocals`, `drums`,
|
|
`bass`, `guitar`, `keyboard`, `percussion`, `strings`, `synth`, `fx`, `brass`,
|
|
`woodwinds`. Non-standard names produce a warning but are passed through.
|
|
|
|
### Server model routing (ace-server only)
|
|
|
|
These fields are parsed by ace-server but are not part of the C++ `AceRequest`
|
|
struct. They select which model to load from the `--models` directory.
|
|
|
|
**`synth_model`** (string, default `""`)
|
|
DiT model filename to use for /synth (e.g. `"acestep-v15-turbo-Q8_0.gguf"`).
|
|
Empty string keeps the currently loaded DiT, or loads the first available one.
|
|
|
|
**`lm_model`** (string, default `""`)
|
|
LM model filename to use for /lm and /understand (e.g. `"acestep-5Hz-lm-4B-Q8_0.gguf"`).
|
|
Empty string keeps the currently loaded LM, or loads the first available one.
|
|
|
|
**`adapter`** (string, default `""`)
|
|
Adapter name from the `--adapters` directory (e.g. `"singer-v2.safetensors"`
|
|
or `"my-peft-adapter"`). Empty string means no adapter. Changing the adapter
|
|
reloads the DiT (deltas are merged into weights at load time). Supported
|
|
algorithm today: LoRA.
|
|
|
|
**`adapter_scale`** (float, default `1.0`)
|
|
Adapter scaling factor. Only used when `adapter` is set.
|
|
|
|
### LM sampling (ace-lm)
|
|
|
|
**`lm_temperature`** (float, default `0.85`)
|
|
Sampling temperature for both phase 1 (lyrics/metadata) and phase 2 (audio
|
|
codes). Lower = more deterministic.
|
|
|
|
**`lm_cfg_scale`** (float, default `2.0`)
|
|
Classifier-Free Guidance scale for the LM. Always active in phase 2 (audio
|
|
code generation). In phase 1, CFG is disabled whenever textual expansion is
|
|
happening (lyrics generation or CoT caption enrichment). In practice CFG
|
|
only applies to phase 1 when lyrics are provided AND `use_cot_caption=false`,
|
|
i.e. the LM is filling metadata fields without any free-text generation.
|
|
`1.0` disables CFG.
|
|
|
|
**`lm_top_p`** (float, default `0.9`)
|
|
Nucleus sampling cutoff. `1.0` disables.
|
|
|
|
**`lm_top_k`** (int, default `0` = disabled)
|
|
Top-K sampling. `0` disables hard top-K (top_p still applies).
|
|
|
|
**`lm_negative_prompt`** (string, default `""`)
|
|
Negative caption for CFG in phase 2. Empty string falls back to a
|
|
caption-less unconditional prompt.
|
|
|
|
**`use_cot_caption`** (bool, default `true`)
|
|
When `true`, the LLM enriches the user caption via CoT and the enriched
|
|
version is written to the output JSON (and fed to the DiT). When `false`,
|
|
the user caption is preserved verbatim. Only matters when the LLM runs
|
|
phase 1 (i.e. some metadata is missing). When all metadata is provided
|
|
phase 1 is skipped and the caption is never touched regardless of this flag.
|
|
|
|
### DiT flow matching (ace-synth)
|
|
|
|
**`inference_steps`** (int, default `0` = auto)
|
|
Number of diffusion denoising steps. `0` resolves from the loaded model:
|
|
turbo = `8`, base/SFT = `50`.
|
|
|
|
**`guidance_scale`** (float, default `0.0` = auto)
|
|
CFG scale for the DiT. `0.0` resolves to `1.0` (CFG disabled).
|
|
Any value > 1.0 on a turbo model is overridden to 1.0 with a warning.
|
|
|
|
**`shift`** (float, default `0.0` = auto)
|
|
Flow-matching schedule shift. Controls the timestep distribution.
|
|
`shift = s*t / (1 + (s-1)*t)`. `0.0` resolves from the loaded model:
|
|
turbo = `3.0`, base/SFT = `1.0`.
|
|
|
|
**`infer_method`** (string, default `""` = ODE Euler)
|
|
Diffusion solver. `""` or `"ode"` uses ODE Euler (one model eval per step,
|
|
same seed always gives same result). `"sde"` uses SDE Stochastic (predicts x0
|
|
then re-noises with fresh Philox noise at each step, producing varied results
|
|
across different trajectories). SDE is reproducible: the per-step noise is
|
|
derived from the original seed so the same seed gives the same SDE trajectory.
|
|
|
|
Turbo preset: `inference_steps=8, guidance_scale=1.0, shift=3.0`.
|
|
Base/SFT preset: `inference_steps=50, guidance_scale=1.0, shift=1.0`.
|
|
|
|
## ace-lm reference
|
|
|
|
```
|
|
Usage: ace-lm --request <json> --lm <gguf> [options]
|
|
|
|
Required:
|
|
--request <json> Input request JSON
|
|
--lm <gguf> 5Hz LM GGUF file
|
|
|
|
Debug:
|
|
--max-seq <N> KV cache size (default: 8192)
|
|
--no-fsm Disable FSM constrained decoding
|
|
--no-fa Disable flash attention
|
|
--no-batch-cfg Split CFG into two separate forwards
|
|
--clamp-fp16 Clamp hidden states to FP16 range
|
|
--dump-logits <path> Dump prefill logits (binary f32)
|
|
--dump-tokens <path> Dump prompt token IDs (CSV)
|
|
```
|
|
|
|
Three LLM sizes: 0.6B (fast), 1.7B, 4B (best quality).
|
|
|
|
Batching is controlled by `lm_batch_size` in the request JSON (default 1).
|
|
Model weights are read once per decode step for all N sequences.
|
|
|
|
## ace-synth reference
|
|
|
|
```
|
|
Usage: ace-synth --request <json...> --embedding <gguf> --dit <gguf> --vae <gguf> [options]
|
|
|
|
Required:
|
|
--request <json...> One or more request JSONs (from ace-lm --request)
|
|
--embedding <gguf> Embedding GGUF file
|
|
--dit <gguf> DiT GGUF file
|
|
--vae <gguf> VAE GGUF file
|
|
|
|
Audio:
|
|
--src-audio <file> Source audio (WAV or MP3)
|
|
--ref-audio <file> Timbre reference audio (WAV or MP3)
|
|
|
|
Adapter:
|
|
--adapter <path> Adapter safetensors file or PEFT directory
|
|
--adapter-scale <float> Adapter scaling factor (default: 1.0)
|
|
|
|
Output:
|
|
--format <fmt> Output format: mp3, wav16, wav24, wav32 (default: mp3)
|
|
--mp3-bitrate <kbps> MP3 bitrate (default: 128)
|
|
|
|
Memory control:
|
|
--vae-chunk <N> Latent frames per tile (default: 256)
|
|
--vae-overlap <N> Overlap frames per side (default: 64)
|
|
|
|
Debug:
|
|
--no-fa Disable flash attention
|
|
--no-batch-cfg Split DiT CFG into two separate forwards
|
|
--clamp-fp16 Clamp hidden states to FP16 range
|
|
--dump <dir> Dump intermediate tensors
|
|
```
|
|
|
|
Models are loaded once and reused across all requests.
|
|
|
|
When `--adapter` is provided, deltas are merged into the DiT projection
|
|
weights at load time (before QKV fusion and GPU upload). For LoRA, the
|
|
safetensors file is parsed directly, each lora_A/lora_B pair is multiplied
|
|
(`alpha/rank * scale * B @ A`), and the result is added to the base weight
|
|
in F32 before requantizing back to the original GGUF type. This is a
|
|
static merge: inference runs at full speed with no adapter overhead.
|
|
`--adapter` accepts either a safetensors file or a directory containing
|
|
`adapter_model.safetensors` and `adapter_config.json` (PEFT format).
|
|
|
|
`--src-audio` provides source content for cover, repaint, lego, extract and
|
|
complete tasks. The audio (WAV or MP3, any sample rate) is resampled to 48kHz
|
|
and VAE-encoded once. `audio_cover_strength` in the JSON controls how many
|
|
DiT steps use the source context (default 1.0). `cover_noise_strength`
|
|
blends the initial noise with source latents to start diffusion closer to
|
|
the source (default 0.0).
|
|
|
|
`--ref-audio` provides a timbre reference, independent of the task. The audio
|
|
is VAE-encoded and fed to the 4-layer timbre encoder, which pools to a single
|
|
embedding via frame[0]. This conditions the DiT to match the tonal quality of
|
|
the reference. When omitted, the timbre encoder receives a single silence
|
|
frame (no timbre conditioning).
|
|
|
|
Batching comes from two sources: multiple `--request` files on the CLI
|
|
(or JSON array on the server), and `synth_batch_size` inside each request.
|
|
Both are combined: 2 request files with `synth_batch_size=3` yields 6 tracks
|
|
in one GPU pass.
|
|
|
|
## ace-server reference
|
|
|
|
HTTP server exposing the same pipelines as `ace-lm`, `ace-synth`, and
|
|
`ace-understand`. One binary, one port.
|
|
|
|
POST /lm, POST /synth, and POST /understand are all **asynchronous**: they
|
|
return a job ID immediately, push the request to a FIFO queue, and the single
|
|
worker thread processes jobs in order. Clients poll GET /job?id=N for status
|
|
and fetch results with GET /job?id=N&result=1.
|
|
Cancel: POST /job?id=N&cancel=1 stops a specific job.
|
|
|
|
`--models` scans a directory for GGUF files and classifies each by its
|
|
`general.architecture` metadata into LM, Text-Enc, DiT, and VAE buckets.
|
|
Each request loads the model, executes, and frees it. With `--keep-loaded`,
|
|
models persist in VRAM and are reused across requests. GPU access is
|
|
serialized by the single worker thread (no mutex needed).
|
|
|
|
| Pipeline | GGUF architectures needed | Enables | VRAM (approx) |
|
|
|:---------|:--------------------------|:--------|:--------------|
|
|
| LM | `acestep-lm` | /lm | ~7 GB (batch=1) |
|
|
| Synth | `acestep-text-enc` + `acestep-dit` + `acestep-vae` | /synth | ~12 GB |
|
|
| Understand | `acestep-lm` + `acestep-dit` + `acestep-vae` | /understand | ~7 GB |
|
|
|
|
Endpoints whose pipeline has no models in the registry return 501.
|
|
|
|
```
|
|
Usage: ace-server --models <dir> [options]
|
|
|
|
Required:
|
|
--models <dir> Directory of GGUF model files
|
|
|
|
Adapter:
|
|
--adapters <dir> Directory of adapters
|
|
|
|
Memory control:
|
|
--keep-loaded Keep models in VRAM between requests
|
|
--vae-chunk <N> Latent frames per tile (default: 256)
|
|
--vae-overlap <N> Overlap frames per side (default: 64)
|
|
|
|
Output:
|
|
--mp3-bitrate <kbps> MP3 bitrate (default: 128)
|
|
|
|
Server:
|
|
--host <addr> Listen address (default: 127.0.0.1)
|
|
--port <N> Listen port (default: 8080)
|
|
--max-batch <N> LM batch limit (default: 1)
|
|
--max-seq <N> KV cache size (default: 8192)
|
|
|
|
Debug:
|
|
--no-fsm Disable FSM constrained decoding
|
|
--no-fa Disable flash attention
|
|
--no-batch-cfg Split CFG into two separate forwards (LM + DiT)
|
|
--clamp-fp16 Clamp hidden states to FP16 range
|
|
```
|
|
|
|
Examples:
|
|
|
|
```bash
|
|
# all models in one directory
|
|
./ace-server --models /path/to/models
|
|
|
|
# with adapters
|
|
./ace-server --models /path/to/models --adapters /path/to/adapters
|
|
|
|
# custom port and batch limit
|
|
./ace-server --models /path/to/models --host 0.0.0.0 --port 8085 --max-batch 2
|
|
```
|
|
|
|
### Endpoints
|
|
|
|
```
|
|
POST /lm Submit LM generation, returns job ID
|
|
POST /lm?mode=inspire Submit inspire generation, returns job ID
|
|
POST /lm?mode=format Submit format generation, returns job ID
|
|
body: application/json AceRequest
|
|
response: {"id":"1"}
|
|
|
|
POST /synth Submit synth generation (MP3), returns job ID
|
|
POST /synth?wav=1 Submit synth generation (WAV), returns job ID
|
|
body: application/json AceRequest or [AceRequest, ...]
|
|
body: multipart/form-data (request + audio + ref_audio)
|
|
response: {"id":"2"}
|
|
|
|
POST /understand Submit understand, returns job ID
|
|
body: multipart/form-data (audio + optional request)
|
|
body: application/json (codes only mode)
|
|
response: {"id":"3"}
|
|
|
|
GET /job?id=N Poll job status
|
|
response: {"status":"running|done|failed|cancelled"}
|
|
|
|
GET /job?id=N&result=1 Fetch job result
|
|
LM/understand: application/json [AceRequest, ...]
|
|
synth jobs: audio/mpeg or audio/wav (single track)
|
|
synth jobs: multipart/mixed (batch, each part is raw audio)
|
|
|
|
POST /job?id=N&cancel=1 Cancel a specific job
|
|
response: {"status":"cancelled"}
|
|
|
|
GET /health Server health check
|
|
response: {"status":"ok"}
|
|
|
|
GET /props Server config, models, presets, defaults
|
|
response: application/json
|
|
|
|
GET /logs SSE stream of server stderr
|
|
response: text/event-stream
|
|
|
|
GET / Embedded WebUI (gzipped HTML)
|
|
```
|
|
|
|
`lm_model`, `synth_model`, `adapter`, `adapter_scale` fields in the JSON body
|
|
select which model and adapter to load. `synth_batch_size` duplicates a
|
|
request for multiple DiT variations (clamped to 9). Error responses are
|
|
JSON: `{"error":"message"}` with 400, 500, 501, or 503 status.
|
|
|
|
**GET /props** returns available models, server configuration, and the
|
|
default AceRequest (source of truth for webui dropdowns and placeholders):
|
|
```json
|
|
{
|
|
"models": {
|
|
"lm": ["acestep-5Hz-lm-0.6B-Q8_0.gguf", "acestep-5Hz-lm-4B-Q8_0.gguf"],
|
|
"embedding": ["Qwen3-Embedding-0.6B-Q8_0.gguf"],
|
|
"dit": ["acestep-v15-turbo-Q8_0.gguf", "acestep-v15-xl-turbo-Q8_0.gguf"],
|
|
"vae": ["vae-BF16.gguf"]
|
|
},
|
|
"adapters": [],
|
|
"cli": { "max_batch": 1, "mp3_bitrate": 128 },
|
|
"default": { "caption": "", "duration": 0, ... }
|
|
}
|
|
```
|
|
|
|
### Concurrency
|
|
|
|
A single GPU mutex serializes all compute. LM and synth workers run in
|
|
detached threads and block on the mutex until the GPU is free. Understand
|
|
uses try_lock and returns 503 instantly if the GPU is busy.
|
|
|
|
Completed jobs are stored in memory (LRU, 10 entries). A disconnected
|
|
client can poll and fetch the result after reconnecting. Each job has
|
|
its own cancel flag so multi-user cancel is safe.
|
|
|
|
By default, each request loads its model, executes, and frees it.
|
|
With `--keep-loaded`, models persist in VRAM and are reused. If the
|
|
requested model differs from the one currently loaded, the old model
|
|
is freed and the new one is loaded before processing.
|
|
|
|
Request bodies are limited to 256 MB (source + reference audio, up to
|
|
10 minutes WAV each).
|
|
|
|
## neural-codec reference
|
|
|
|
GGML-native neural audio codec based on the Oobleck VAE encoder and decoder.
|
|
Serves two purposes: validating the precision of the full VAE chain (encode +
|
|
decode roundtrip), and compressing music at 6.8 kbit/s with no perceptible
|
|
difference from the original.
|
|
|
|
```
|
|
Usage: neural-codec --vae <gguf> --encode|--decode -i <input> [-o <o>] [--q8|--q4]
|
|
|
|
Required:
|
|
--vae <path> VAE GGUF file
|
|
--encode | --decode Encode audio to latent, or decode latent to WAV
|
|
-i <path> Input (WAV/MP3 for encode, latent for decode)
|
|
|
|
Output:
|
|
-o <path> Output file (auto-named if omitted)
|
|
--q8 Quantize latent to int8 (~13 kbit/s)
|
|
--q4 Quantize latent to int4 (~6.8 kbit/s)
|
|
--format <fmt> WAV format: wav16, wav24, wav32 (default: wav16)
|
|
|
|
Output naming: song.wav -> song.latent (f32) or song.nac8 (Q8) or song.nac4 (Q4)
|
|
song.latent -> song.wav
|
|
|
|
Memory control:
|
|
--vae-chunk <N> Latent frames per tile (default: 256)
|
|
--vae-overlap <N> Overlap frames per side (default: 64)
|
|
|
|
Latent formats (decode auto-detects):
|
|
f32: flat [T, 64] f32, no header. ~51 kbit/s.
|
|
NAC8: header + per-frame Q8. ~13 kbit/s.
|
|
NAC4: header + per-frame Q4. ~6.8 kbit/s.
|
|
```
|
|
|
|
The encoder is the symmetric mirror of the decoder: same snake activations,
|
|
same residual units, strided conv1d for downsampling instead of transposed
|
|
conv1d for upsampling. No new GGML ops. Downsample 2x4x4x6x10 = 1920x.
|
|
|
|
48kHz stereo audio is compressed to 64-dimensional latent frames at 25 Hz.
|
|
Three output formats, decode auto-detects from file content:
|
|
|
|
| Format | Frame size | Bitrate | 3 min song | vs f32 (cossim) |
|
|
|--------|-----------|---------|------------|-----------------|
|
|
| f32 | 256B | 51 kbit/s | 1.1 MB | baseline |
|
|
| NAC8 | 66B | 13 kbit/s | 290 KB | 0.9999 |
|
|
| NAC4 | 34B | 6.8 kbit/s | 150 KB | 0.989 |
|
|
|
|
NAC = Neural Audio Codec. The NAC8 and NAC4 file formats are headerless
|
|
except for a 4-byte magic (`NAC8` or `NAC4`) and a uint32 frame count.
|
|
Q8 quantization error is 39 dB below the VAE reconstruction error (free).
|
|
Q4 quantization error is 16 dB below the VAE reconstruction error (inaudible
|
|
on most material).
|
|
|
|
```bash
|
|
# encode (Q4: 6.8 kbit/s, ~150 KB for 3 minutes)
|
|
./neural-codec --vae models/vae-BF16.gguf --encode --q4 -i song.wav -o song.nac4
|
|
|
|
# encode (Q8: 13 kbit/s, ~290 KB for 3 minutes)
|
|
./neural-codec --vae models/vae-BF16.gguf --encode --q8 -i song.wav -o song.nac8
|
|
|
|
# decode (auto-detects format)
|
|
./neural-codec --vae models/vae-BF16.gguf --decode -i song.nac4 -o song_decoded.wav
|
|
|
|
# roundtrip validation: compare song.wav and song_decoded.wav with your ears
|
|
```
|
|
|
|
## mp3-codec reference
|
|
|
|
Standalone MIT-licensed MPEG1 Layer III encoder and decoder. No external
|
|
dependencies. The encoder is used by `ace-synth` for MP3 output. The decoder
|
|
uses minimp3 (CC0). Reads WAV or MP3, writes WAV or MP3 (auto-detected
|
|
from output extension).
|
|
|
|
```
|
|
Usage: mp3-codec -i <input> -o <o> [options]
|
|
|
|
-i <path> Input file (WAV or MP3)
|
|
-o <path> Output file (WAV or MP3)
|
|
-b <kbps> Bitrate for MP3 encoding (default: 128)
|
|
--format <f> WAV format: wav16, wav24, wav32 (default: wav16)
|
|
|
|
Mode is auto-detected from output extension.
|
|
|
|
Examples:
|
|
mp3-codec -i song.wav -o song.mp3
|
|
mp3-codec -i song.wav -o song.mp3 -b 192
|
|
mp3-codec -i song.mp3 -o song.wav
|
|
mp3-codec -i song.mp3 -o song.wav --format wav32
|
|
```
|
|
|
|
## ace-understand reference
|
|
|
|
Reverse pipeline: audio (or pre-existing audio codes) -> LM understand ->
|
|
metadata + lyrics. The output JSON is reusable as ace-lm or ace-synth input.
|
|
|
|
Two input modes: `--src-audio` runs the full chain (VAE encode + FSQ tokenize +
|
|
LM), `--request` with an `audio_codes` field skips straight to the LM.
|
|
|
|
```
|
|
Usage: ace-understand [--src-audio <file> --dit <gguf> --vae <gguf> | --request <json>] --lm <gguf>
|
|
|
|
Audio input (full pipeline):
|
|
--src-audio <file> Source audio (WAV or MP3, any sample rate)
|
|
--dit <gguf> DiT GGUF (for FSQ tokenizer weights + silence_latent)
|
|
--vae <gguf> VAE GGUF (for audio encoding)
|
|
|
|
Code input (skip VAE + tokenizer):
|
|
--request <json> Request JSON with audio_codes field
|
|
|
|
Required:
|
|
--lm <gguf> 5Hz LM GGUF file
|
|
|
|
Output:
|
|
-o <json> Output JSON (default: stdout summary)
|
|
|
|
Sampling params (lm_temperature, lm_top_p, lm_top_k) come from the
|
|
request JSON. Without --request, understand defaults apply
|
|
(temperature=0.3, top_p disabled).
|
|
|
|
Memory control:
|
|
--vae-chunk <N> Latent frames per tile (default: 256)
|
|
--vae-overlap <N> Overlap frames per side (default: 64)
|
|
|
|
Debug:
|
|
--max-seq <N> KV cache size (default: 8192)
|
|
--no-fsm Disable FSM constrained decoding
|
|
--no-fa Disable flash attention
|
|
--dump <dir> Dump tok_latents + tok_codes (skip LM)
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
ace-lm (Qwen3 causal LM, 0.6B/1.7B/4B)
|
|
Phase 1 (if needed): CoT generates bpm, keyscale, timesignature, lyrics
|
|
Phase 2: audio codes (5Hz tokens, FSQ vocabulary)
|
|
Both phases batched: N sequences per forward, weights read once
|
|
CFG with dual KV cache per batch element (cond + uncond)
|
|
Output: request0.json .. requestN-1.json
|
|
|
|
ace-synth
|
|
BPE tokenize
|
|
Qwen3-Embedding (28L text encoder)
|
|
CondEncoder (lyric 8L + timbre 4L + text_proj)
|
|
FSQ detokenizer (audio codes -> flow matching source latents)
|
|
Adapter merge (optional: LoRA safetensors delta -> dequant/merge/requant at load)
|
|
DiT (2B: 24L H=2048, XL: 32L H=2560, flow matching ODE Euler or SDE Stochastic)
|
|
VAE (AutoencoderOobleck, tiled decode)
|
|
WAV stereo 48kHz
|
|
|
|
ace-understand (reverse pipeline)
|
|
Audio read (WAV/MP3, any rate -> 48kHz stereo)
|
|
VAE encode (tiled, AutoencoderOobleck encoder)
|
|
FSQ tokenize (latent -> 5Hz codes via 2L attention pooler)
|
|
Qwen3 LM (understand prompt: codes -> CoT metadata + lyrics)
|
|
FSM constrains CoT fields, audio codes blocked after </think>
|
|
No CFG, no batch. Single sequence, greedy-ish (temperature=0.3)
|
|
Output: JSON with caption, lyrics, bpm, key, duration, language
|
|
```
|
|
|
|
## LM specifics
|
|
|
|
ace-lm is not a general-purpose chat engine. It is a two-phase autoregressive
|
|
pipeline specialized for ACE-Step music generation.
|
|
|
|
Phase 1 (CoT) generates structured metadata (bpm, keyscale, timesignature, caption,
|
|
duration, language) and optionally lyrics via chain-of-thought reasoning. An FSM
|
|
(finite state machine) built from a prefix tree enforces valid field names and values
|
|
at every decode step, hard-masking invalid tokens before sampling.
|
|
|
|
Phase 2 (audio codes) generates 5Hz FSQ tokens. The FSQ codec uses levels
|
|
[8,8,8,5,5,5] producing 64000 distinct codes (8*8*8*5*5*5). The tokenizer
|
|
reserves 65535 slots (audio_code_0 to audio_code_65534) appended to the base
|
|
Qwen3 vocabulary; the 1535 extra slots are unused by the codec. A partial LM
|
|
head projects only the audio code subrange of the embedding matrix, cutting
|
|
the output GEMM by 70% compared to full-vocab projection.
|
|
Classifier-free guidance (CFG) is fused into the batch dimension: N
|
|
conditional and N unconditional sequences are packed into a single forward pass
|
|
(2*N tokens, one weight read), then combined as
|
|
`logits = uncond + scale * (cond - uncond)`. The KV cache is a single 4D tensor
|
|
`[D, max_seq, Nkv, n_sets]` shared across all batch elements and CFG paths. Shared
|
|
prompts are prefilled once and cloned to other KV sets via copy, avoiding redundant
|
|
prefills.
|
|
|
|
## Accuracy
|
|
|
|
Test logs (turbo + SFT, seed 42, Philox noise, multiple quantizations):
|
|
[`tests/`](https://github.com/ServeurpersoCom/acestep.cpp/tree/master/tests)
|
|
|
|
Each script compares GGML C++ output against the Python reference
|
|
(cosine similarity per intermediate tensor). Requires the original
|
|
ACE-Step-1.5 repo cloned alongside acestep.cpp (`../ACE-Step-1.5`).
|
|
|
|
```bash
|
|
cd tests
|
|
python3 debug-lm-logits.py # Qwen3 LM: first-token logits GGML vs PyTorch (0.6B/1.7B/4B)
|
|
python3 debug-detok-cossim.py # FSQ detokenizer: step-by-step cossim C++ vs Python
|
|
python3 debug-dit-cossim.py # DiT: per-layer cossim GGML vs Python (turbo/SFT, BF16/quantized)
|
|
```
|
|
|
|
## Patched GGML fork
|
|
|
|
Uses a patched GGML fork (submodule) with two new ops, a Metal im2col optimization, and
|
|
a CUDA bugfix for the Oobleck VAE decoder. All backends: CPU, CUDA, ROCm, Metal, Vulkan.
|
|
F32/F16/BF16 data types. The DiT uses only standard GGML ops and needs no patches.
|
|
|
|
The VAE reconstructs audio from latent space through 5 upsampling blocks (total 1920x),
|
|
each running a transposed convolution followed by 3 WaveNet-style residual units with
|
|
dilated convolutions and Snake activations. A single tile builds a graph of 36 snake
|
|
activations, 5 transposed convolutions, and 32 regular convolutions. At the final blocks,
|
|
sequence lengths reach 491520 timesteps, which stresses GGML ops designed for short NLP
|
|
sequences.
|
|
|
|
### `GGML_OP_SNAKE` (fused Snake activation)
|
|
|
|
Computes y = x + sin^2(a * x) * inv_b in a single kernel.
|
|
The Oobleck VAE calls this 36 times per tile. Without a fused op, each activation
|
|
requires 5 separate GGML kernels (mul, sin, sqr, mul, add), causing 5x the memory
|
|
traffic. The fused kernel reads x once and writes y once. BF16 cast nodes before/after
|
|
each snake call halve memory bandwidth at the cost of negligible precision loss
|
|
(cossim > 0.999 vs F32 baseline).
|
|
|
|
### `GGML_OP_COL2IM_1D` (scatter-add for GEMM-based conv_transpose_1d)
|
|
|
|
Gather-based reconstruction of a 1D signal from GEMM columns [K*OC, T_in] to
|
|
[T_out, OC], with fused padding crop via the p0 parameter.
|
|
Upstream `ggml_conv_transpose_1d` uses a naive kernel (one scalar FMA loop per output
|
|
element, no shared memory, no tensor cores). The VAE spends 40% of its FLOP budget on
|
|
transposed convolutions. We decompose each as `mul_mat + col2im_1d`, routing the heavy
|
|
GEMM through cuBLAS/BLAS/MPS tensor cores. The col2im_1d gather has a 2-iteration inner
|
|
loop and is pure bandwidth. BF16 cast nodes around col2im_1d halve the scatter bandwidth.
|
|
|
|
### Metal: `kernel_im2col_1d` (flat 1D dispatch)
|
|
|
|
The generic Metal `kernel_im2col` dispatches (IC, 1, OW) threadgroups with K threads
|
|
each. For the VAE's 1D convolutions with small kernels (k=1 or k=7), this wastes 78-97%
|
|
of SIMD lanes (7 or 1 active threads per 32-wide SIMD group). The dedicated
|
|
`kernel_im2col_1d` uses a flat dispatch identical to snake and col2im_1d:
|
|
(total/256, 1, 1) threadgroups with 256 threads, achieving full SIMD utilization.
|
|
The dispatch branches on `is_2D` at runtime; the 2D path and kernel are unchanged.
|
|
CUDA and Vulkan already use flat dispatch and are not affected.
|
|
|
|
VAE decode (M2 Pro 16GB, 86.8s audio @ 48kHz stereo):
|
|
|
|
| chunk | overlap | im2col | tiles | time |
|
|
|------:|--------:|-----------|------:|-------:|
|
|
| 256 | 64 | generic | 17 | 71.2s |
|
|
| 1024 | 16 | generic | 3 | 38.9s |
|
|
| 256 | 64 | im2col_1d | 17 | 31.8s |
|
|
| 1024 | 16 | im2col_1d | 3 | 18.3s |
|
|
|
|
### Bugfix: `im2col` gridDim.y overflow (CUDA)
|
|
|
|
Upstream `im2col_kernel` uses OW directly as grid dimension Y, which exceeds the CUDA
|
|
65535 gridDim limit on long sequences. The VAE calls `ggml_conv_1d` (im2col path) 32
|
|
times per tile at output widths up to 491520. Fixed with a grid-stride loop on OW and
|
|
`MIN(OW, MAX_GRIDDIM_Z)` clamping.
|
|
|
|
### Upstream divergence
|
|
|
|
The GGML submodule diverges from upstream only by the addition of
|
|
`GGML_OP_SNAKE` and `GGML_OP_COL2IM_1D`. No existing upstream kernel is
|
|
modified. These ops are required; the VAE does not work without them.
|
|
|
|
An earlier approach patched the upstream naive ops instead of adding custom
|
|
ones. Those patches were dropped. They are documented here in case someone
|
|
wants to study the naive path:
|
|
|
|
- `conv_transpose_1d`: bounded loop replacing O(T_in) brute-force, CUDA and Metal
|
|
- `im2col`: grid-stride loop on OW to fix gridDim.y overflow for large tensors
|