Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
974 lines
58 KiB
Markdown
974 lines
58 KiB
Markdown
# LFM2.5-Audio
|
|
|
|
LFM2.5-Audio is Liquid AI's end-to-end speech and text model. audio.cpp runs its
|
|
speech recognition, text-to-speech and spoken chat (speech-to-speech) as the
|
|
community family `lfm2_audio`, directly from the GGUFs Liquid AI publishes, for
|
|
the English and the Japanese checkpoint.
|
|
|
|
Upstream: [LiquidAI/LFM2.5-Audio-1.5B](https://huggingface.co/LiquidAI/LFM2.5-Audio-1.5B) ·
|
|
[LiquidAI/LFM2.5-Audio-1.5B-JP](https://huggingface.co/LiquidAI/LFM2.5-Audio-1.5B-JP) ·
|
|
reference implementation: [liquid-audio](https://github.com/Liquid4All/liquid-audio)
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Family | `lfm2_audio` |
|
|
| Tasks | `asr`, `tts`, `s2s` |
|
|
| Modes | `offline`; `streaming` for TTS and S2S |
|
|
| Languages | `en` (`LFM2.5-Audio-1.5B`), `ja` (`LFM2.5-Audio-1.5B-JP`); each checkpoint transcribes, speaks and chats in its own language |
|
|
| Audio input (ASR, S2S) | WAV at any sample rate; channels are averaged and the audio is resampled to 16 kHz |
|
|
| Speech output (TTS, S2S) | 24 kHz mono speech; S2S also returns the reply's text |
|
|
| Voices | TTS: `us_male` (default), `us_female`, `uk_male`, `uk_female` for English; the Japanese checkpoint has one voice. S2S replies in the checkpoint's own voice |
|
|
| Backends tested | CPU, CUDA, Metal |
|
|
| License | LFM Open License v1.0 |
|
|
|
|
## Architecture
|
|
|
|
- NeMo 128-bin log-mel frontend and a FastConformer encoder (17 layers, 8x
|
|
depthwise-striding subsampling, relative-position attention) based on
|
|
canary-180m-flash, followed by an MLP adapter. The frontend configuration is
|
|
the same as `canary_asr`.
|
|
- LFM2 hybrid backbone: 10 gated short-convolution blocks and 6 grouped-query
|
|
attention blocks, with RMSNorm, QK-norm and SwiGLU. The text head is tied to
|
|
the token embedding.
|
|
- ASR: greedy decoding of the chat prompt liquid-audio builds: the system
|
|
prompt `Perform ASR.` (`Perform ASR in japanese.` for the JP checkpoint),
|
|
then the audio embeddings as the user turn, until `<|im_end|>`.
|
|
- TTS: the system prompt `Perform TTS. Use the US male voice.` (one per voice;
|
|
`Perform TTS in japanese.` for the JP checkpoint) and the text as the user
|
|
turn. After `<|audio_start|>` each backbone step yields one 80 ms audio
|
|
frame: a 6-layer depthformer predicts its 8 codes one codebook at a time,
|
|
and the frame goes back into the backbone as the sum of its code embeddings,
|
|
until a frame starts with the end-of-audio code. A frame that picks
|
|
end-of-audio for another codebook goes back in too but has no sound, so, as
|
|
in liquid-audio's demo, it is not decoded.
|
|
- S2S: liquid-audio's interleaved generation. The system prompt `Respond with
|
|
interleaved text and audio.` and the user's audio as the user turn; the reply
|
|
alternates 6 text tokens and 12 audio frames (9 for the JP checkpoint, whose
|
|
vocoder GGUF records its blocks) until `<|text_end|>`, continues with audio
|
|
until end-of-audio, and ends at `<|im_end|>`. Text is greedy and audio is
|
|
sampled at temperature 1.0 with top-k 4, as in liquid-audio's README and chat
|
|
demo; `text_temperature` and `text_top_k` sample the text as
|
|
`generate_interleaved`'s arguments of the same names do. A later turn of a
|
|
conversation puts the earlier questions and replies before the new question,
|
|
as liquid-audio's ChatState does ([Conversations](#conversations)).
|
|
- An LFM2 detokenizer (8 layers, causal sliding-window attention over 30 steps)
|
|
turns the mean of each frame's code embeddings, repeated 6 times, into
|
|
log-magnitude and phase, and an ISTFT (n_fft 1280, hop 320) gives 24 kHz
|
|
audio.
|
|
|
|
## Packages
|
|
|
|
Liquid AI publishes each quantization as four llama.cpp-format GGUFs in
|
|
[LiquidAI/LFM2.5-Audio-1.5B-GGUF](https://huggingface.co/LiquidAI/LFM2.5-Audio-1.5B-GGUF/tree/7d525f883a077e20afb782f2ff618edcae0e39e4)
|
|
and [LiquidAI/LFM2.5-Audio-1.5B-JP-GGUF](https://huggingface.co/LiquidAI/LFM2.5-Audio-1.5B-JP-GGUF/tree/64b96718b341dbd5650f9e85627cecdcbd4ac61b).
|
|
The packages point at those repositories, pinned to the linked revisions:
|
|
|
|
| File | Contents | Used by |
|
|
|---|---|---|
|
|
| `<model>-<quant>.gguf` | LFM2 backbone and text tokenizer | ASR, TTS, S2S |
|
|
| `mmproj-<model>-<quant>.gguf` | Audio encoder and adapter; the embedding of generated audio codes | ASR, TTS, S2S |
|
|
| `vocoder-<model>-<quant>.gguf` | Depthformer; the detokenizer's code embedding and ISTFT window | TTS, S2S |
|
|
| `tokenizer-<model>-<quant>.gguf` | Audio detokenizer | TTS, S2S |
|
|
|
|
| Package | Checkpoint | Weights | Download |
|
|
|---|---|---|---|
|
|
| `lfm2_audio_1_5b_q8_0` (default) | EN | Q8_0 | 1.8 GB |
|
|
| `lfm2_audio_1_5b_f16` | EN | F16 | 3.3 GB |
|
|
| `lfm2_audio_1_5b_q4_0` | EN | Q4_0 | 1.1 GB |
|
|
| `lfm2_audio_1_5b_jp_q8_0` | JP | Q8_0 | 1.8 GB |
|
|
| `lfm2_audio_1_5b_jp_f16` | JP | F16 | 3.3 GB |
|
|
| `lfm2_audio_1_5b_jp_f32` | JP | F32 | 6.5 GB |
|
|
| `lfm2_audio_1_5b_jp_q4_0` | JP | Q4_0 | 1.1 GB |
|
|
|
|
```bash
|
|
python3 tools/model_manager_v2.py install lfm2_audio_1_5b_q8_0 --models-root models
|
|
python3 tools/model_manager_v2.py install lfm2_audio_1_5b_jp_q8_0 --models-root models
|
|
```
|
|
|
|
All quantizations of a checkpoint install into one directory,
|
|
`models/LFM2.5-Audio-1.5B-GGUF` or `models/LFM2.5-Audio-1.5B-JP-GGUF`, so
|
|
components of different quantizations can be combined with the session options
|
|
below. The WebUI lists both checkpoints under ASR, TTS and speech-to-speech with
|
|
their Q8_0 and F16 packages; speech-to-speech has no text box, so replies use
|
|
liquid-audio's system prompt. A package installed before audio.cpp had
|
|
LFM2-Audio TTS holds only the backbone and mmproj GGUFs; TTS and S2S then name
|
|
the missing files, and installing it again with `--overwrite` brings all four.
|
|
|
|
The WebUI's speech-to-speech page holds a conversation: each Run sends the
|
|
recorded or chosen question to `/v1/tasks/stream` with the earlier turns, as
|
|
[Conversations](#conversations) describes, plays the reply as it streams in
|
|
and adds the turn to a list, and New conversation starts over. With
|
|
`--ui-management` the page loads the entry in streaming mode itself. With a
|
|
config file, live playback needs the entry in `"mode": "streaming"`; an
|
|
offline entry gives each reply when it is done.
|
|
|
|
## Run
|
|
|
|
```bash
|
|
audiocpp_cli --task asr --family lfm2_audio \
|
|
--model models/LFM2.5-Audio-1.5B-GGUF --backend cuda --audio speech.wav
|
|
|
|
audiocpp_cli --task tts --family lfm2_audio \
|
|
--model models/LFM2.5-Audio-1.5B-GGUF --backend cuda \
|
|
--text "The next train leaves in ten minutes." --voice-id uk_female --out speech.wav
|
|
|
|
audiocpp_cli --task s2s --family lfm2_audio \
|
|
--model models/LFM2.5-Audio-1.5B-GGUF --backend cuda \
|
|
--audio question.wav --out reply.wav --text-out reply.txt
|
|
```
|
|
|
|
`--model` is the package directory: the published GGUFs do not embed an
|
|
audio.cpp model spec, so a GGUF file path is rejected. With one quantization
|
|
installed, its backbone is used with the `mmproj-`, `vocoder-` and
|
|
`tokenizer-` files of the same name. With several, choose the backbone:
|
|
|
|
```bash
|
|
audiocpp_cli --task asr --family lfm2_audio \
|
|
--model models/LFM2.5-Audio-1.5B-GGUF --backend cuda --audio speech.wav \
|
|
--session-option lfm2_audio.model_gguf=LFM2.5-Audio-1.5B-F16.gguf
|
|
```
|
|
|
|
For Japanese, pass `models/LFM2.5-Audio-1.5B-JP-GGUF` and no voice. `language`
|
|
can be left out; a value other than the checkpoint's language is rejected.
|
|
Speech is sampled like the README's example (temperature 0.8, top-k 64); pass
|
|
`--seed` for repeatable audio, or `--temperature 0` for greedy decoding.
|
|
|
|
S2S answers a spoken turn with a spoken reply and its text: the CLI prints the
|
|
text (`text_output=`) and `--text-out` writes it. Alone, a request is a new
|
|
conversation under liquid-audio's chat system prompt, `Respond with interleaved
|
|
text and audio.`; `--text` replaces that prompt, which the checkpoints were
|
|
trained with, so leave it out unless experimenting. The reply is sampled like
|
|
liquid-audio's README and demo (temperature 1.0, top-k 4) and may run to 1024
|
|
steps, text tokens and audio frames together, about a minute of speech. Use the
|
|
Q8_0 package, the default, or F16 for S2S: with Q4_0, 2 of 30 replies to one
|
|
English question on CUDA and 2 of 30 on the M3 Ultra CPU (2 of 60 on the x86
|
|
CPU, none on Metal) repeated a sentence until `max_tokens`; with Q8_0 and F16
|
|
none did (see [Validation](#s2s)). A reply that reaches `max_tokens` is cut off
|
|
there, its text and speech kept, as liquid-audio keeps what it generated, and a
|
|
warning goes to stderr. Greedy audio (`--temperature 0`) can go on speaking
|
|
after the text until `max_tokens`, as liquid-audio's greedy decoding does: to
|
|
one Japanese question both gave the same text and 972 audio frames, all 1024
|
|
steps. Text tokens stand for bytes, not characters. Bytes that make no whole
|
|
character, which replies in scripts such as Thai can have, come out as U+FFFD,
|
|
as the Hugging Face tokenizer decodes them, in replies and transcripts alike; a
|
|
character left open at the end of the text is dropped, as in a transcript cut
|
|
at `max_tokens`. A request can also carry the turns before it; see
|
|
[Conversations](#conversations).
|
|
|
|
The question goes through the encoder in one pass, as in liquid-audio, so it
|
|
can be at most `lfm2_audio.max_pass_duration_sec` long (120 s by default; see
|
|
[Long audio](#long-audio)): a longer one is rejected before it is encoded.
|
|
|
|
The server takes the same directory and session options:
|
|
|
|
```json
|
|
{"models": [{"id": "lfm2-audio-asr", "family": "lfm2_audio", "task": "asr", "mode": "offline",
|
|
"path": "models/LFM2.5-Audio-1.5B-GGUF",
|
|
"session_options": {"lfm2_audio.model_gguf": "LFM2.5-Audio-1.5B-Q8_0.gguf"}}]}
|
|
```
|
|
|
|
```bash
|
|
audiocpp_server --backend cuda --config server.json
|
|
curl http://127.0.0.1:8080/v1/audio/transcriptions -F model=lfm2-audio-asr -F file=@speech.wav
|
|
```
|
|
|
|
A TTS entry is the same with `"task": "tts"`, served at `/v1/audio/speech`:
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8080/v1/audio/speech -H 'Content-Type: application/json' \
|
|
-d '{"model": "lfm2-audio-tts", "input": "The next train leaves in ten minutes.", "voice": "us_female"}' -o speech.wav
|
|
```
|
|
|
|
An S2S entry (`"task": "s2s"`) answers at `/v1/tasks/run` with the reply's
|
|
`text` and its audio as base64 WAV (`audio`):
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8080/v1/tasks/run -H 'Content-Type: application/json' \
|
|
-d '{"model": "lfm2-audio-s2s", "request": {"audio": "/path/to/question.wav"}}'
|
|
```
|
|
|
|
### Streaming TTS
|
|
|
|
With `--mode streaming` (CLI) or `"mode": "streaming"` (server entry), speech
|
|
comes out as it is generated. Like liquid-audio's demo, each audio frame is
|
|
decoded as soon as the depthformer picks it. The detokenizer is causal and
|
|
carries each layer's state from frame to frame (the attention layers' last 29
|
|
keys and values and the short-conv layers' last inputs), so a frame costs
|
|
only its own six steps, and the ISTFT releases a sample once no later window
|
|
reaches it. Each event carries one frame (80 ms, `stream_frames_per_event` to
|
|
change it), plus a last 20 ms event per text chunk; the events add up to the
|
|
offline speech for the same seed, up to the detokenizer's arithmetic (below).
|
|
A text chunk that reaches `max_tokens` ends its speech there, streamed as
|
|
offline, with the same warning.
|
|
|
|
```bash
|
|
curl -N http://127.0.0.1:8080/v1/audio/speech -H 'Content-Type: application/json' \
|
|
-d '{"model": "lfm2-audio-tts-stream", "input": "The next train leaves in ten minutes.", "voice": "us_female", "stream": true}'
|
|
```
|
|
|
|
The response is server-sent events with base64 16-bit PCM deltas at 24 kHz;
|
|
`"stream_format": "audio"` returns the raw PCM instead.
|
|
|
|
Time to the first audio and real-time factor through the server, for a
|
|
two-sentence English text (6.5 s of speech) and a Japanese one (8.5 s); the
|
|
M3 Ultra CPU's Q8_0 and Q4_0 figures are with ggml's repacked kernels, see
|
|
[CPU](#cpu):
|
|
|
|
| Backend | First audio, F16 / Q8_0 / Q4_0 / JP F32 | RTF, 1 frame per event | RTF, 4 frames per event |
|
|
|---|---|---|---|
|
|
| CUDA, NVIDIA A10 | 39 / 27 / 24 / 45 ms | 0.09-0.21 | 0.09-0.21 |
|
|
| Metal, Apple M3 Ultra | 40 / 37 / 35 / 52 ms | 0.19-0.29 | 0.19-0.27 |
|
|
| CPU, Apple M3 Ultra, 16 threads | 150 / 54 / 48 / 308 ms | 0.16-0.57 | 0.16-0.57 |
|
|
|
|
A frame per event costs no more than four, and streaming about what offline
|
|
speech does. The streamed audio differs from offline (relative RMS) by the
|
|
detokenizer's arithmetic in graphs of other sizes. With F16 that is 5e-4 to
|
|
6e-3 on the test's short text across the backends; on CUDA, whose cuBLAS
|
|
accumulates the offline products in half precision, it is 5e-3 to 3.2e-2 over
|
|
English and Japanese texts of 1.5 to 18 s and 2e-2 over a minute of speech.
|
|
Quantized packages round the activations to 8 bits on the CPU and CUDA, and
|
|
graphs of other sizes round them differently: on CUDA, over texts of 2.5 to
|
|
19 s, the stream differs by 2.2e-2 to 4.6e-2 with Q8_0 and by 0.06 to 0.32 with
|
|
Q4_0 (3.4e-2 and 0.22 over a minute). The stream decodes a frame at a
|
|
time, and at that size ggml-cuda multiplies Q4_0 weights with its MMVQ kernel,
|
|
whose dot product makes the rounding error two to three times larger than in
|
|
the kernel offline decoding uses. By log-spectral distance, the offline Q4_0
|
|
audio on CUDA is the closer to exact arithmetic with the same weights (0.21 to
|
|
0.26 dB against 0.53 to 0.64 dB for the stream).
|
|
|
|
### Streaming S2S
|
|
|
|
With `--mode streaming` (CLI) or `"mode": "streaming"` (server entry), the
|
|
question comes in as audio chunks, live PCM included (`--audio -` on the CLI).
|
|
The reply starts when the input ends and streams like TTS: each event carries
|
|
the audio of one frame (`stream_frames_per_event`) and the text written since
|
|
the last event, in whole characters. Together they are the offline reply for
|
|
the same seed: the same text, and the same audio up to the detokenizer's
|
|
arithmetic, as for TTS. On CUDA, streamed replies to
|
|
`assets/resources/c.wav` differ from offline by 4.3e-2 with F16 (one reply),
|
|
2.2e-2 to 4.5e-2 with Q8_0 and 0.06 to 0.15 with Q4_0 (five replies each). A
|
|
reply cut off at `max_tokens` streams as it does offline, with the same
|
|
warning; a character left open at the cut is dropped in both. A stream
|
|
finished before its reply is over returns the text its events carried and,
|
|
with `return_codes`, the reply's steps so far.
|
|
A question longer than `lfm2_audio.max_pass_duration_sec` fails as soon as that
|
|
much of it has come, without waiting for the rest.
|
|
The server's live route, `/v1/audio/speech/live`, takes the question as
|
|
chunked raw PCM. Without `input` the reply uses liquid-audio's system prompt;
|
|
an `input` replaces it, as `--text` does.
|
|
|
|
```bash
|
|
ffmpeg -i question.wav -ar 16000 -ac 1 -f s16le - \
|
|
| curl -N -X POST -H 'Expect:' -T - \
|
|
'http://127.0.0.1:8080/v1/audio/speech/live?model=lfm2-audio-s2s-stream&sample_rate=16000&channels=1&sample_format=s16le&return_text=true'
|
|
```
|
|
|
|
It returns the reply's audio as server-sent events and, with `return_text=true`,
|
|
its text: `speech.text.delta` events carry what the reply wrote since the
|
|
previous event, ahead of the audio that speaks it, and `speech.text.done` the
|
|
whole text. The query also takes the request options `seed`, `temperature`,
|
|
`top_k`, `text_temperature`, `text_top_k` and `max_tokens`. Time from the end
|
|
of a 7.5 s English question, streamed in real time, to the first audio of the
|
|
reply (the M3 Ultra CPU's Q8_0 and Q4_0 figures with ggml's repacked kernels,
|
|
see [CPU](#cpu)):
|
|
|
|
| Backend | F16 | Q8_0 | Q4_0 |
|
|
|---|---|---|---|
|
|
| CUDA, NVIDIA A10 | 107 ms | 106 ms | 73 ms |
|
|
| Metal, Apple M3 Ultra | 183 ms | 168 ms | 153 ms |
|
|
| CPU, Apple M3 Ultra, 16 threads | 478 ms | 228 ms | 205 ms |
|
|
|
|
That covers encoding the whole question, the prompt, the first text block and
|
|
the first frame. With the JP F32 package on CUDA, the first audio came 113 ms
|
|
after a 2.6 s Japanese question. Offline, `/v1/tasks/run` returned a 13.1 s
|
|
English reply in 2.1 s on CUDA (F16).
|
|
|
|
## Conversations
|
|
|
|
liquid-audio's chat demo keeps every turn of a conversation and prefills all of
|
|
it again for each reply. audio.cpp gives each reply the same prompt, but the
|
|
client keeps the conversation: it sends the earlier turns with each new
|
|
question, as the request's input artifacts, in conversation order. For each
|
|
earlier turn, its question and then its reply:
|
|
|
|
| Artifact id | Kind | Payload |
|
|
|---|---|---|
|
|
| `lfm2_audio.question` | `custom` | The question's audio as a WAV file, with the samples that turn's request had (a float WAV holds any exactly) |
|
|
| `lfm2_audio.reply` | `acoustic_tokens` | The reply artifact that turn's result returned, payload and meta unchanged |
|
|
|
|
The request's audio is the new question. With `return_codes=true` the result,
|
|
offline or streamed, also returns the reply as an `lfm2_audio.reply` artifact
|
|
for the next turn. The prompt is the one ChatState holds in liquid-audio's
|
|
README multi-turn example: the system prompt once, each earlier question and
|
|
its reply in the chat markup, then the new question. A reply goes back in step
|
|
by step, its text as the token ids it generated and its frames as the sum of
|
|
their code embeddings, as liquid-audio's `_prefill` takes them.
|
|
|
|
This prompt differs from the chat demo's on purpose. `demo/chat.py`, as
|
|
written, adds the system turn again after the open user turn on every turn,
|
|
since its `len(chat.text) == 1` check always holds; audio.cpp keeps it once.
|
|
In ten-turn English conversations run with liquid-audio, replies under the
|
|
repeated system turn answered 1 of 21 questions about earlier turns, and 7 of
|
|
21 with the system prompt once.
|
|
|
|
The reply artifact's payload is little-endian int32 values, one step after
|
|
another: a text token is its id, and an audio frame is -1 followed by its
|
|
8 codes. It holds every step of the reply, `<|text_end|>`, the frame that
|
|
ends the audio and frames with end-of-audio in another codebook included, as
|
|
liquid-audio keeps them: about 470 bytes per second of reply audio. Its meta:
|
|
|
|
| Key | Value |
|
|
|---|---|
|
|
| `format` | `lfm2_audio.reply/1` |
|
|
| `language` | The checkpoint's language, `en` or `ja` |
|
|
| `codebooks`, `audio_vocab_size`, `text_vocab_size` | The checkpoint's codebooks (8), codes per codebook with end-of-audio (2049) and text vocabulary (65536) |
|
|
| `steps` | Text tokens and frames in the payload |
|
|
| `ended` | `true` when the reply ended; `false` when `max_tokens`, or a stream finished early, cut it off |
|
|
|
|
A reply that was cut off goes back in as it is, as liquid-audio keeps it. The
|
|
artifact holds ids and codes only, so it replays on any quantization of the
|
|
same checkpoint. A reply from the other checkpoint or in another format
|
|
version is rejected, and so are artifacts out of turn, artifacts with other
|
|
ids, meta the format does not have and a question that does not read as WAV.
|
|
These are errors in the request, as are a `return_codes` that is neither true
|
|
nor false and history or `return_codes` sent to ASR or TTS: the server answers
|
|
each with 400 (`invalid_request_error`).
|
|
|
|
The system prompt, `text_input` or the default, applies to the whole
|
|
conversation, so send the same one every turn. A turn's reply depends only on
|
|
the system prompt, the earlier turns, the new question and the request
|
|
options, seed included: on a backend, the same request gives the same bytes
|
|
whatever ran before it. A turn with earlier turns prefills in blocks of
|
|
256 steps, so its attention never holds more than 256 scores per prompt step
|
|
and head. Each earlier question is held to `lfm2_audio.max_pass_duration_sec`, as
|
|
the new one is.
|
|
|
|
So that a turn does not run the whole conversation again, an S2S session keeps
|
|
what the last turn of a conversation ran, for up to
|
|
`lfm2_audio.conversation_cache_slots` conversations at a time (4 by default):
|
|
|
|
- The questions of a conversation's last request with history or
|
|
`return_codes`, as mono 16 kHz samples, and their encoder output: about
|
|
160 KiB per second of question. An earlier question with exactly the samples
|
|
of a kept one takes the kept output instead of going through the encoder.
|
|
The new question always goes through it.
|
|
- The full 256-step blocks of the prefill of a conversation's last turn with
|
|
history: the keys and values each block wrote, 24 KiB per step over the
|
|
backbone's six attention layers, the conv state after it, 160 KiB per block,
|
|
and its input, which holds 8 KiB per step of question audio. That is 6 to 8
|
|
MiB per block, the most for a block of question audio, and about 70 MiB at the
|
|
tenth turn of a conversation of 2900 steps. A turn's prompt starts with the
|
|
turn before's, so its prefill restores the blocks it starts with that have the
|
|
same input, bit for bit, and runs the rest: the end of the turn before's last
|
|
block, its reply and the new question. The last block always runs.
|
|
|
|
At the 8192-step limit, a conversation that is nearly all question audio
|
|
takes about 350 MiB of the session's host memory: about 190 MiB of keys,
|
|
values and conv state, about 60 MiB of block inputs and about 100 MiB of
|
|
questions. The session keeps up to that for each slot, the blocks that
|
|
conversations share counted once, and a slot that no conversation has used
|
|
takes nothing. A request whose questions are not the kept ones holds both until
|
|
it has encoded them all, up to about 100 MiB more for that time. After each
|
|
turn with history, the timing log (`--log` or `--log-file`) reports how many
|
|
prompts' blocks are kept, one per conversation or two for one that changed or
|
|
left out turns until the older is dropped, and the MiB they take
|
|
(`lfm2_audio.prefill.kept_prompts` and `lfm2_audio.prefill.kept_mib`).
|
|
|
|
A turn gives the same bytes with or without them; they only save time. They stay
|
|
until a request replaces them or the session closes; resetting a stream does
|
|
not free them. A request finds what was kept for its conversation by content:
|
|
|
|
- A turn with history restores the blocks its prompt starts with from the kept
|
|
conversation whose blocks start with the most of them. When it is that
|
|
conversation's next turn, or the same turn again, its own blocks after them
|
|
go on the end of that conversation's; otherwise they are kept as a new
|
|
conversation's. So conversations that start the same, such as with one long
|
|
system prompt or the same first turns, share those blocks, and each keeps its
|
|
own from where they part. A turn that changes an earlier turn keeps its
|
|
blocks as a new conversation's, and the one it came from is dropped when it
|
|
is the least recently used. Until then it takes a slot, so another
|
|
conversation's blocks can be dropped first; when clients leave out or change
|
|
earlier turns, set one slot more than the conversations. A turn whose prompt
|
|
is under 256 steps keeps no blocks.
|
|
- A request with history or `return_codes` keeps its questions in place of
|
|
those of its conversation: the kept questions whose last one it also asks,
|
|
the ones that hold the most of its earlier questions if several do.
|
|
Otherwise they are kept as a new conversation's.
|
|
|
|
A new conversation drops the least recently used conversation's blocks or
|
|
questions past the slots, each on its own: a first turn with `return_codes`
|
|
keeps questions but no blocks. With 0 slots, nothing is kept. With more
|
|
conversations taking turns than slots, they push each other out: with one slot,
|
|
a turn of another conversation in between makes the next turn of this one
|
|
encode its earlier questions again and run its prefill again from where the two
|
|
differ. A request without history or `return_codes` keeps nothing and
|
|
leaves what the session kept. A turn that leaves out the oldest turns moves
|
|
every position after them, so its prefill runs in full and its blocks are kept
|
|
as a new conversation's, as above, though its questions still take the kept
|
|
encoder output; leave out several turns at once rather than one per turn, so
|
|
that fewer turns run in full. A first turn runs as before and keeps no blocks,
|
|
so the third turn of a conversation is the first to resume a prefill. Send each
|
|
question as the samples its turn had, which a float WAV holds exactly: one that
|
|
differs goes through the encoder again, and the prefill runs again from the
|
|
block it starts in.
|
|
|
|
A conversation's prompt and `max_tokens` may take 8192 steps together, where a
|
|
step is 80 ms of a question's audio, a text token or an 80 ms reply frame. Past
|
|
that the turn fails with a CapacityError as soon as the request shows it: when
|
|
the request is read, from the replies and the chat markup, so that a stream
|
|
fails at its start, before its audio, or else as the questions are encoded.
|
|
The client then leaves out the oldest turns; audio.cpp never leaves out any
|
|
itself. A first turn is held only to the context. Conversations were checked up
|
|
to 10 turns, with prompts of up to about 5000 steps at the tenth: replies kept
|
|
their speech and used what earlier turns said, but did not recall what users
|
|
said about themselves, such as their name, and late Japanese replies sometimes
|
|
repeated an earlier one. Past 10 turns, leave out the oldest turns.
|
|
|
|
Through the C API (`include/audiocpp.h`), the reply artifact is the result's
|
|
first artifact, from `audiocpp_session_run` or `audiocpp_stream_finish`, and
|
|
goes into the next request with its meta:
|
|
|
|
```c
|
|
/* Turn 1, with return_codes. */
|
|
audiocpp_request_set_audio(request_1, question_1, frames_1, sample_rate, channels);
|
|
audiocpp_request_set_option(request_1, "return_codes", "true");
|
|
audiocpp_session_run(session, request_1, &result_1);
|
|
|
|
/* Turn 2: the first question's WAV, then the reply artifact and its meta. */
|
|
audiocpp_artifact_kind kind;
|
|
const char * id;
|
|
const void * payload;
|
|
size_t bytes, index;
|
|
audiocpp_result_artifact(result_1, 0, &kind, &id, &payload, &bytes);
|
|
audiocpp_request_add_artifact(request_2, AUDIOCPP_ARTIFACT_CUSTOM, "lfm2_audio.question",
|
|
wav_1, wav_1_bytes, NULL);
|
|
audiocpp_request_add_artifact(request_2, kind, id, payload, bytes, &index);
|
|
for (size_t m = 0; m < audiocpp_result_artifact_meta_count(result_1, 0); ++m) {
|
|
const char * key;
|
|
const char * value;
|
|
audiocpp_result_artifact_meta(result_1, 0, m, &key, &value);
|
|
audiocpp_request_set_artifact_meta(request_2, index, key, value);
|
|
}
|
|
audiocpp_request_set_audio(request_2, question_2, frames_2, sample_rate, channels);
|
|
audiocpp_request_set_option(request_2, "return_codes", "true");
|
|
audiocpp_session_run(session, request_2, &result_2);
|
|
```
|
|
|
|
A third turn sends both earlier questions and replies, in order, and so on.
|
|
|
|
The server returns the reply artifact in the `artifacts` array of
|
|
`/v1/tasks/run` and `/v1/tasks/stream` results (base64 `payload`, and `meta`)
|
|
when the request's `options` have `"return_codes": true`. A later request sends
|
|
the history back in its own `artifacts` array, in the same shape: per earlier
|
|
turn, in order, an `lfm2_audio.question` of kind `custom` holding the
|
|
question's WAV bytes, then the `lfm2_audio.reply` that turn returned, as it
|
|
came back. Either can give its bytes as a base64 `payload` or a `path` to a
|
|
file. The new question goes in `audio` as usual, and the reply's `meta` goes
|
|
back exactly as the result returned it:
|
|
|
|
```json
|
|
{"model": "lfm2-audio-s2s", "request": {
|
|
"audio": "/path/to/turn2.wav",
|
|
"options": {"return_codes": true},
|
|
"artifacts": [
|
|
{"id": "lfm2_audio.question", "kind": "custom", "path": "/path/to/turn1.wav"},
|
|
{"id": "lfm2_audio.reply", "kind": "acoustic_tokens", "payload": "<turn 1's base64>",
|
|
"meta": {"format": "lfm2_audio.reply/1", "language": "en", "codebooks": "8",
|
|
"audio_vocab_size": "2049", "text_vocab_size": "65536",
|
|
"steps": "<turn 1's steps>", "ended": "<turn 1's ended>"}}
|
|
]}}
|
|
```
|
|
|
|
The CLI's `--request-sequence` JSON takes the same `artifacts` array. The CLI
|
|
writes result artifacts with `payload_hex`, which requests do not take, so
|
|
write the bytes to a file and pass its `path`. The live route,
|
|
`/v1/audio/speech/live`, takes no artifacts: each of its requests is a first
|
|
turn.
|
|
|
|
`/v1/tasks/stream` with `"stream_format": "sse"` sends each reply while it is
|
|
generated: `task.stream.event` events carry its text and audio as in
|
|
[Streaming S2S](#streaming-s2s), and the closing `task.stream.done` event
|
|
carries the result with the `lfm2_audio.reply` artifact (see the
|
|
[server README](../../app/server/README.md#post-v1tasksstream)). The entry
|
|
needs `"mode": "streaming"`; an offline entry fails on this route. The done
|
|
event repeats the whole reply's audio as one Base64 WAV on a single line. A
|
|
client that closes the stream before `task.stream.done` gets no reply
|
|
artifact, so it has to leave that turn out of its next request's history. The
|
|
route does not read `audio_base64`, so the new question goes in `audio` as a
|
|
path the server can read; the earlier questions can still be Base64 payloads.
|
|
A turn the model turns away, such as one past the step limit, is an `error`
|
|
event before any audio, with the message that the JSON response carries with
|
|
HTTP 400.
|
|
|
|
Every request to a server entry runs on that entry's one session, whatever its
|
|
route, also with `--parallel-jobs`, which runs `lfm2_audio` requests one at a
|
|
time. So clients that take turns on one S2S entry share its
|
|
`lfm2_audio.conversation_cache_slots`: the default of 4 keeps four
|
|
conversations. For more that go on at the same time, set it to at least their
|
|
number:
|
|
|
|
```json
|
|
{"id": "lfm2-audio-s2s", "family": "lfm2_audio", "path": "/path/to/LFM2.5-Audio-1.5B-GGUF",
|
|
"task": "s2s", "mode": "streaming",
|
|
"session_options": {"lfm2_audio.model_gguf": "LFM2.5-Audio-1.5B-Q4_0.gguf",
|
|
"lfm2_audio.conversation_cache_slots": "8"}}
|
|
```
|
|
|
|
Each kept conversation adds the host memory above while the server runs: about
|
|
75 MiB at the tenth turn of a conversation of 2900 steps, and up to about
|
|
350 MiB at the step limit. The server's `min_free_memory_mb` guard estimates a
|
|
model from its files when it loads, so it does not count that memory.
|
|
|
|
## Request Options (use with `--request-option`)
|
|
|
|
| Option | Task | Default | Meaning |
|
|
|---|---|---|---|
|
|
| `language` | all | The checkpoint's | `en` or `ja`; must match the checkpoint. |
|
|
| `max_tokens` | all | `512`; S2S `1024` | ASR: transcript tokens per audio chunk. TTS: 80 ms audio frames per text chunk. S2S: text tokens and audio frames of the reply together. A transcript, a text chunk's speech or a reply that reaches it is cut off there and kept, as liquid-audio keeps it, and a warning goes to stderr; the other chunks go on. Text tokens need not end on a character boundary, so a cut can fall inside a character: liquid-audio then shows U+FFFD for the partial character, while audio.cpp drops it and ends the transcript or reply at the last whole character. On the CPU, `max_tokens` can change an output at a near-tie even where it is not reached, so a cut transcript, speech or reply is not always the start of the uncut one: the decode cache's length follows `max_tokens`, and ggml's CPU flash attention splits a cache of 512 steps or more into one piece per thread. |
|
|
| `audio_chunk_mode` | ASR | `auto` | `auto`, `vad`, `fixed` or `none`; see [Long audio](#long-audio). |
|
|
| `audio_chunk_duration_sec` | ASR | `30`, or `lfm2_audio.max_pass_duration_sec` if lower | Longest chunk in seconds, at least 1. Legacy `audio_chunk_seconds` remains accepted. |
|
|
| `temperature` | TTS, S2S | `0.8`; S2S `1.0` | Audio code sampling temperature; 0 is greedy. |
|
|
| `top_k` | TTS, S2S | `64`; S2S `4` | Sample from the k most likely codes; 0 keeps all, 1 is greedy. |
|
|
| `text_temperature` | S2S | `0` | Text token sampling temperature; 0 is greedy, as in liquid-audio's README and demo. |
|
|
| `text_top_k` | S2S | `0` | With `text_temperature` above 0, sample from the k most likely text tokens; 0 keeps all, 1 is greedy. With 0, any token can be drawn, control tokens included, so keep a top-k with a high temperature. |
|
|
| `seed` | TTS, S2S | Random | Sampling seed; TTS text chunk i uses seed + i. S2S text sampling draws from a stream of its own, so with the same seed a reply follows the greedy-text reply until a text token differs. |
|
|
| `text_chunk_mode` | TTS | `japanese` for JP, else `default` | How long text is split; see [Long text](#long-text). |
|
|
| `text_chunk_size` | TTS | `200` | Unicode codepoints per text chunk. |
|
|
| `stream_frames_per_event` | TTS, S2S streaming | `1` | Audio frames (80 ms) per streaming event. |
|
|
| `return_codes` | S2S | `false` | Also return the reply's text tokens and audio codes as an `lfm2_audio.reply` artifact, to send back as history; see [Conversations](#conversations). |
|
|
|
|
Each task rejects the options it does not take.
|
|
|
|
## Session Options (use with `--session-option`)
|
|
|
|
| Option | Default | Meaning |
|
|
|---|---|---|
|
|
| `lfm2_audio.model_gguf` | The directory's only backbone | Backbone GGUF, relative to the model directory. |
|
|
| `lfm2_audio.mmproj_gguf` | `mmproj-<backbone file>`, else the only mmproj | Audio encoder GGUF, relative to the model directory. |
|
|
| `lfm2_audio.vocoder_gguf` | `vocoder-<backbone file>`, else the only vocoder | TTS depthformer GGUF, relative to the model directory. |
|
|
| `lfm2_audio.detokenizer_gguf` | `tokenizer-<backbone file>`, else the only tokenizer | TTS audio detokenizer GGUF, relative to the model directory. |
|
|
| `lfm2_audio.vad_model_path` | `assets/framework/models/silero_vad` | Silero VAD model used to split long audio. |
|
|
| `lfm2_audio.max_pass_duration_sec` | `120` | Most audio, in seconds, the encoder takes in one pass: an ASR chunk (the whole input with `audio_chunk_mode=none`) or the S2S question; at least 1. Longer audio is rejected before it is encoded; see [Long audio](#long-audio). Legacy `lfm2_audio.max_pass_seconds` remains accepted. |
|
|
| `lfm2_audio.conversation_cache_slots` | `4` | S2S: how many conversations a session keeps the earlier questions' encoder output and prefill blocks of, for their next turns; the least recently used is dropped first, and `0` keeps none. Output never depends on it; see [Conversations](#conversations). |
|
|
| `lfm2_audio.cpu_repack` | `true` | On the CPU, multiply the matrices with the kernels of ggml's CPU extra buffer types where those take the weight's type and shape: the repacked ones take Q4_0 on x86 with AVX2 and Q4_0, Q8_0 and Q6_K on Arm; where ggml's AMX buffer is built, it comes first and also takes F16, Q8_0 and K-quants. Where the token embedding's type is taken, the text head gets a copy of it. `false` keeps every weight in the plain CPU buffer. Other backends ignore it; see [CPU](#cpu). |
|
|
|
|
## Long Audio
|
|
|
|
liquid-audio transcribes a whole file in one pass. The English model handles
|
|
that up to about a minute; longer audio loses words and then repeats itself.
|
|
The Japanese model holds to about 40 s; past that it starts to skip whole
|
|
sentences, and at 60 s it leaves out 18 to 29% of the text. audio.cpp keeps
|
|
short input whole and splits longer input:
|
|
|
|
| Mode | Behavior |
|
|
|---|---|
|
|
| `auto` | Input up to `audio_chunk_duration_sec` is transcribed whole, as liquid-audio does. Longer input is split at pauses found by the bundled Silero VAD, and silence between speech is skipped. Without the VAD model, it falls back to `fixed`. |
|
|
| `vad` | Always splits at pauses; fails without the VAD model. |
|
|
| `fixed` | Cuts every `audio_chunk_duration_sec`; a last piece shorter than 1 s and the chunk before it split their audio evenly, so no chunk is longer than `audio_chunk_duration_sec`. |
|
|
| `none` | One pass over the whole input, as liquid-audio does; input longer than `lfm2_audio.max_pass_duration_sec` is rejected. |
|
|
|
|
Each chunk is transcribed on its own, and the transcripts are joined with a
|
|
space between English words and without one in Japanese text. Audio the VAD
|
|
does not mark as speech is not transcribed. That is usually silence, but it
|
|
can be speech too quiet for the VAD: a sentence recorded over 30 dB below the
|
|
others around it, or a whole 40 s file turned down by 60 dB (7 of 8 such files
|
|
came back empty). A split can also land on a short pause inside a sentence,
|
|
and the model may then leave out the words that open the next chunk (1 of 12
|
|
such sentences in the Japanese tests below).
|
|
|
|
No pass takes more than `lfm2_audio.max_pass_duration_sec` of audio, 120 s by
|
|
default. `none` rejects longer input. The other modes reject an
|
|
`audio_chunk_duration_sec` above the limit
|
|
on input longer than the limit, before any chunk is planned or encoded, so
|
|
`vad` is refused on the setting even when its spans would be shorter; a request
|
|
that leaves `audio_chunk_duration_sec` unset gets chunks of the limit when that is
|
|
under 30 s. One pass is of little use past 120 s: `none` is at 29% WER on the
|
|
120 s English files below, and Japanese loses sentences from about 41 to 45 s.
|
|
The encoder's buffer grows linearly, at about 6.46 MB per second of audio, up
|
|
to about 240 s (775 MB at 120 s), and with the square of the length beyond
|
|
that (8.8 GB measured at 600 s on an A10); without the limit, on the CPU or
|
|
with unified memory, nothing stops a long pass before allocation. The server
|
|
answers such a request with 400. The limit is a session option, so the
|
|
operator can raise it (`--session-option` on the CLI, `session_options` in a
|
|
server entry), and a task request cannot (with `--ui-management`,
|
|
`/v1/models/load` sets session options like any server entry).
|
|
|
|
Word error rate by input length, EN F16 on CUDA, 8 files per length built from
|
|
consecutive LibriSpeech test-clean utterances joined with 0.3 s gaps (each file
|
|
up to about 5 s longer than its length):
|
|
|
|
| Length | liquid-audio (one pass) | `auto` | `fixed` |
|
|
|---|---|---|---|
|
|
| 30 s | 2.8% | 2.3% | 2.9% |
|
|
| 40 s | 1.7% | 1.7% | 1.8% |
|
|
| 50 s | 1.7% | 1.5% | 2.5% |
|
|
| 60 s | 2.1% | 2.0% | 2.8% |
|
|
| 90 s | 9.4% | 1.3% | 2.7% |
|
|
| 120 s | 83% | 1.9% | 2.6% |
|
|
| 180 s | 296% | 1.4% | 2.4% |
|
|
|
|
`none` follows liquid-audio up to 90 s (2.6%, 1.7%, 1.7%, 2.0% and 9.4%).
|
|
At 120 s it finishes all 8 files at 29%, 6 of them word for word as
|
|
liquid-audio. At 180 s, 7 of the 8 reach `max_tokens` and come back cut off
|
|
there, repetitions included, with a warning. Those runs predate the limit:
|
|
with the default, `none` now rejects the 120 s files (up to 125 s long) and the
|
|
180 s ones.
|
|
|
|
Character error rate by input length, JP F32 on CUDA, 8 files per length built
|
|
from consecutive Common Voice ja test clips (one sentence each) joined with
|
|
0.3 s gaps. The clips keep their own silence at both ends, so sentences are
|
|
about 1.5 s apart and `auto` cuts the files over 30 s about one sentence per
|
|
chunk; each file is up to 3.7 s shorter than its length, so `auto` keeps the
|
|
20 and 30 s files whole. Punctuation and spaces are removed before scoring.
|
|
The model's own errors on the single clips, often a different kanji or number
|
|
form (1500円 for 千五百円), already come to 9 to 12%. In parentheses: sentences
|
|
lost (less than half of their characters in the transcript) out of all
|
|
sentences.
|
|
|
|
| Length | liquid-audio (one pass) | `auto` |
|
|
|---|---|---|
|
|
| 20 s | 16.6% (1 of 32) | 16.6% (1 of 32) |
|
|
| 30 s | 13.7% (1 of 46) | 13.7% (1 of 46) |
|
|
| 40 s | 15.2% (1 of 61) | 12.7% (0 of 61) |
|
|
| 45 s | 15.4% (4 of 65) | 10.8% (0 of 65) |
|
|
| 50 s | 24.0% (10 of 73) | 11.3% (0 of 73) |
|
|
| 60 s | 36.1% (26 of 89) | 10.3% (0 of 89) |
|
|
| 90 s | 177% (81 of 131) | 10.0% (0 of 131) |
|
|
|
|
The sentence lost at 20 to 40 s is the same recording in each file, over
|
|
30 dB quieter than the others; liquid-audio drops it from a 16 s file too. At
|
|
90 s one file repeats itself up to the test's 2,048-token cap. With `auto`,
|
|
Q8_0 on CUDA and on Metal is within 0.3 points of F32. `none` gives
|
|
liquid-audio's transcripts with F32 on CUDA (71 of 71 files at 20 to 90 s,
|
|
compared without punctuation and spaces); on the repeating file it reaches
|
|
`max_tokens` and returns the repetition cut off there, with a warning.
|
|
|
|
The limit follows the seconds, not the amount of text. Files built other ways
|
|
(FLEURS ja clips, or Common Voice clips in reverse order, with 0.8 s gaps, with
|
|
their silence trimmed or played 1.3 times faster) lost no other sentence
|
|
entirely (under 30% of it transcribed) in one pass up to 40 s, and those run
|
|
further first lost one between 41 and 45 s; the fastest also kept under half of
|
|
a few sentences from 34 s. For whole sentences, 30 s chunks leave about 10 s of
|
|
margin. Where pauses are short, `auto` packs chunks of 26 to 30 s: with the
|
|
clips trimmed to their speech and joined with pauses of about 0.4 s, it gives
|
|
12.1 to 12.9% at 31 to 35 s (one pass 11.6 to 12.6% on the same files) and 9.8
|
|
to 10.9% at 45 to 180 s, with no repeats, though it can still drop a sentence
|
|
now and then. All of these files are recordings of read sentences; the
|
|
Japanese TTS voice's speech lost sentences in one pass from 28 s.
|
|
|
|
## Long Text
|
|
|
|
liquid-audio speaks a whole text in one turn. audio.cpp splits text longer than
|
|
`text_chunk_size` codepoints at sentence boundaries with the framework's text
|
|
chunker and speaks each chunk as its own turn in the same voice, then joins the
|
|
audio. At the default 200 codepoints a chunk is about 13 s of English speech.
|
|
The shared 6,000-character long-form test text (`tools/audiocpp_cli/audiocpp_cli_longform_tts_clone_cases.json`)
|
|
comes out as about 345 s of speech that transcribes back through the ASR task
|
|
at 1.5-1.6% WER with the EN F16, Q8_0 and Q4_0 packages on CUDA.
|
|
|
|
Each chunk is its own take: the voice stays, but pitch, pace and loudness can
|
|
shift from one chunk to the next (up to about 3 semitones, 20% and 4 dB on a
|
|
7-chunk English text), which can sound like a new recording. liquid-audio's
|
|
chunks shift the same way, and the model cannot carry context across them. In
|
|
one turn, liquid-audio speaks English reliably up to about 430 characters and
|
|
starts to add or drop words from about 600; given the previous chunk as an
|
|
earlier turn, it answered in text instead of speaking on up to 2 of 7 chunks.
|
|
For English, a larger `text_chunk_size` means fewer joins: 300, 400 and 600
|
|
codepoints transcribed back as well as 200 (0.5-1.5% WER). Keep the default for
|
|
Japanese: at 400 its chunks ran past `max_tokens`.
|
|
|
|
## Validation
|
|
|
|
### ASR
|
|
|
|
Transcripts were compared with liquid-audio
|
|
([`19e65845`](https://github.com/Liquid4All/liquid-audio/tree/19e65845923a7f136442c95137884ec61eb386aa),
|
|
fp32, greedy) on 200 English utterances from LibriSpeech test-clean and 200
|
|
Japanese utterances from the Common Voice 8.0 ja test set, for every package,
|
|
on CPU and CUDA (NVIDIA A10) and on CPU and Metal (Apple M3 Ultra):
|
|
|
|
| Weights | Transcripts identical to liquid-audio |
|
|
|---|---|
|
|
| F32 (JP), F16 | 198-200 of 200 |
|
|
| Q8_0 | 193-197 of 200 |
|
|
| Q4_0 | 160-170 of 200 |
|
|
|
|
At F32 and F16, the few differences are near-ties in liquid-audio's own
|
|
output, where its two best tokens are within 0.04 in log-probability. Below
|
|
that, the quantized weights change close token choices. On the x86 CPU a Q8_0
|
|
transcript at such a tie can also depend on `--threads` and `max_tokens`: on
|
|
one Japanese S2S reply whose two best tokens in liquid-audio are 0.04 apart,
|
|
the Q8_0 package added a sentence at 4, 5, 7 and 8 threads but not at 1, 2, 3
|
|
and 6, nor at 4 threads with `max_tokens` 128 or 1024. F32 and F16 gave the
|
|
same transcript at every thread count, and Q8_0 on the M3 Ultra CPU, Metal and
|
|
CUDA added the sentence in every run.
|
|
|
|
Stage by stage on CPU with the F32 weights, the adapter output matches
|
|
liquid-audio within 1.4e-6 relative error and the first-step logits within 9e-7.
|
|
`test_lfm2_audio_asr` (`ENGINE_BUILD_MODEL_TESTS`) checks the prompt, the
|
|
tokenizer, the stage numbers (English F16) and the transcripts against
|
|
liquid-audio: two LibriSpeech clips with the English packages and, with the
|
|
Japanese ones, a Common Voice clip that every JP package transcribes exactly
|
|
on the CPU, Metal and CUDA. It runs when `lfm2_audio_1_5b_f16` is installed in
|
|
`models/` and skips otherwise; for a JP package, pass
|
|
`--model models/LFM2.5-Audio-1.5B-JP-GGUF --model-gguf LFM2.5-Audio-1.5B-JP-Q8_0.gguf`
|
|
(or the F16, F32 or Q4_0 file).
|
|
The `lfm2_audio_*_test` unit tests run on small synthetic GGUFs.
|
|
|
|
Real-time factor over each 200-utterance set (processing time divided by audio
|
|
length, with the model loaded; the CPU runs used 16 threads; the M3 Ultra's
|
|
Q4_0 figure is with ggml's repacked kernels and the x86 row without them, see
|
|
[CPU](#cpu)):
|
|
|
|
| Backend | EN F16 | JP F32 | EN Q4_0 |
|
|
|---|---|---|---|
|
|
| CUDA, NVIDIA A10 | 0.027 | 0.037 | 0.017 |
|
|
| Metal, Apple M3 Ultra | 0.034 | 0.042 | 0.021 |
|
|
| CPU, Linux x86-64 | 0.11 | 0.14 | 0.077 |
|
|
| CPU, Apple M3 Ultra | 0.10 | 0.17 | 0.032 |
|
|
|
|
### TTS
|
|
|
|
liquid-audio (fp32, CUDA with TF32 off) was dumped at every stage for three
|
|
cases: "The quick brown fox jumps over the lazy dog." (US female), "What is
|
|
this obsession people have with books?" (UK male) and a Japanese sentence,
|
|
with greedy decoding (41, 32 and 45 frames):
|
|
|
|
| Stage | JP F32, CPU | F16, Linux CPU and CUDA |
|
|
|---|---|---|
|
|
| Backbone output at `<|audio_start|>` | 1.4e-7 relative | 2e-4 to 9e-4 |
|
|
| First frame's depthformer logits | 5e-7 | 4e-4 to 3e-3 |
|
|
| Greedy frames identical to liquid-audio | all | all for UK male and Japanese; US female until a near-tie (top two logits 0.002 and 0.000 apart) |
|
|
| Detokenizer head on the reference frames | 1.4e-6 | 3e-4 to 5e-3 |
|
|
| Waveform | 6e-6 | 1.3e-3 to 1.9e-2 |
|
|
|
|
The detokenizer's log-magnitudes reach 4.7, a magnitude of 114, which the
|
|
framework's Vocos ISTFT would clamp at 100, so the ISTFT here is the
|
|
reference's. Long audio is detokenized in chunks that overlap by the model's
|
|
receptive field (97 steps); with F32 weights the chunked output matches one
|
|
pass to 3e-5. A stream carries each layer's state instead, and with F32
|
|
weights its waveform matches one pass to 1e-6 (relative RMS).
|
|
`test_lfm2_audio_tts` checks the prompt, the stage numbers, the greedy frames,
|
|
the chunking, the stream, a round trip through ASR, greedy and sampled (two of
|
|
three seeds have to come back word for word), and speech cut off at
|
|
`max_tokens`, offline and streamed; it runs when
|
|
`lfm2_audio_1_5b_f16` is installed in `models/`.
|
|
|
|
Real-time factor through `audiocpp_server` (processing time divided by audio
|
|
length, model loaded; short = three sentences of 3-7 s, long = the 345 s
|
|
long-form text; the CPU runs used 16 threads; the M3 Ultra's short Q8_0 and
|
|
Q4_0 figures are with ggml's repacked kernels, from [CPU](#cpu), on its three
|
|
sentences of 2.5 to 5 s, and the x86 row is without them):
|
|
|
|
| Backend | EN F16 short / long | EN Q8_0 short / long | EN Q4_0 short / long | JP F32 short |
|
|
|---|---|---|---|---|
|
|
| CUDA, NVIDIA A10 | 0.19 / 0.15 | 0.16 / 0.13 | 0.16 / 0.086 | 0.21 |
|
|
| Metal, Apple M3 Ultra | 0.21 / 0.21 | 0.18 / 0.18 | 0.15 / 0.15 | 0.26 |
|
|
| CPU, Linux x86-64 | 0.42 / 0.41 | 0.29 / 0.30 | 0.22 | 0.65 |
|
|
| CPU, Apple M3 Ultra | 0.32 | 0.20 | 0.16 | 0.56 |
|
|
|
|
Each frame takes a backbone step and eight small depthformer steps. On GPUs
|
|
per-step overhead dominates, so the weights' size matters little; on the CPU
|
|
each frame reads the backbone once and the depthformer eight times, which
|
|
shows in F16. Transcribed back by the ASR task, the long-form speech scored
|
|
1.4-1.9% WER on every backend and package measured, mostly homophones and
|
|
spellings such as "their" and "harbour"; the two Japanese test sentences came
|
|
back exactly.
|
|
|
|
With the Q4_0 package on Metal on an Apple M3 Max, the server's footprint was 1.17 GB and
|
|
stayed within 4 MB of that over 12 requests alternating 2.5 s and 57 s of
|
|
speech.
|
|
|
|
### S2S
|
|
|
|
liquid-audio's `generate_interleaved` (fp32, CUDA with TF32 off) was dumped at
|
|
every step for its README's two questions (English and Japanese) and for
|
|
`assets/resources/c.wav`, greedy and sampled like the README (temperature 1.0,
|
|
top-k 4, seed 0), and the same steps were replayed through audio.cpp:
|
|
|
|
| Stage | JP F32, CPU | EN F16, CPU / CUDA |
|
|
|---|---|---|
|
|
| Prompt ids and audio positions | identical | identical |
|
|
| Text logits at every text step | 4e-6 relative | 7e-4 / 3e-3 |
|
|
| Backbone output at every audio step | 4e-7 | 5e-4 / 1.4e-3 |
|
|
| Depthformer logits at every frame | 7e-6 | 1.5e-3 / 3e-3 |
|
|
| Free-running steps identical to liquid-audio | all 277 greedy and 308 sampled | the whole 250-step greedy reply to `c.wav` on the CPU, 248 on CUDA; sampled replies until a near-tie |
|
|
| Waveform of the reference frames | 5e-5 | 9e-3 / 0.12 |
|
|
|
|
The sampled replies were compared through torch's own draws: fed
|
|
liquid-audio's logits, the framework's torch-compatible sampler picks every
|
|
code torch drew. audio.cpp itself samples with its own generator, so a seed
|
|
does not reproduce a liquid-audio reply. The CUDA waveform differs more because
|
|
CUDA accumulates F16 products in half precision.
|
|
|
|
A reply's audio says what its text says, with rare exceptions. Over 30 seeds
|
|
of replies to `c.wav` with the F16, Q8_0 and Q4_0 packages, on the CPU and Metal
|
|
(Apple M3 Ultra) and on the CPU and CUDA (x86-64, NVIDIA A10), the ASR task
|
|
heard the text at a median 4-7% WER, the names the model makes up aside.
|
|
Whisper large-v3-turbo heard nearly every reply at 18% or less; one x86 CPU
|
|
reply spoke a question its text did not have. The ASR task sometimes answers a
|
|
reply, paraphrases it or runs on instead of transcribing it: on 0 to 6 of 30
|
|
replies per backend and package, and on 5 of 60 with liquid-audio fp32. It can
|
|
also add a sentence that was never spoken to speech that sounds like an
|
|
assistant. Over 450 Japanese replies to 13 questions, this happened only after
|
|
the ending 「何かご質問があれば、いつでもお手伝いしますよ!」 (or 「ね!」), where the
|
|
model is close to a tie between ending the transcript and going on. Of the 50
|
|
replies with that ending, liquid-audio fp32 added a sentence, mostly
|
|
「何かご質問はありますか?」, to 7, the F16 package to 6 or 7 of the same replies,
|
|
and the Q8_0 package to 11 on every backend (the CPUs at 8 threads).
|
|
liquid-audio with the Q8_0 weights added one to the same 11, so the four extra
|
|
come from the quantized weights. With F16 and Q8_0, none of the other 400
|
|
replies gained a sentence. With Q4_0, 2 of 30 replies on the M3 Ultra CPU and 2
|
|
of 30 on CUDA repeated a sentence until `max_tokens` (2 of 60 on the x86 CPU,
|
|
none on Metal). F16 and Q8_0 did not, though one Q8_0 reply (M3 Ultra CPU, 8
|
|
threads) finished its text but not its audio within `max_tokens`.
|
|
`test_lfm2_audio_s2s` checks the prompt, the first text and audio blocks, the
|
|
round trip of three replies through ASR (their median, a reply that fails or is
|
|
cut off at `max_tokens` counting as a miss), well-formed text, streaming
|
|
against offline, a stream finished early, a reply cut off at `max_tokens`,
|
|
offline and streamed, that the first request, sent again after the others,
|
|
gets the same reply (S2S sizes its decode cache from the request alone, as TTS
|
|
does), that a reply with `text_temperature` is step for step the
|
|
greedy-text reply of the same seed until a text token differs, and a
|
|
conversation: the first reply's artifact holds its text and speech, a second
|
|
turn (`assets/resources/a.wav`) sent with the first gets another reply than
|
|
without it and the same bytes when sent again, and a third turn replays both;
|
|
it runs when `lfm2_audio_1_5b_f16` is installed in `models/`.
|
|
`lfm2_audio_chat_test` runs the S2S session on a synthetic package, offline
|
|
and streamed, over three turns of a conversation, and up to the conversation
|
|
limit. In one session, the four turns of a longer conversation, another
|
|
conversation in between and a turn over the limit give the replies and errors
|
|
of a fresh session, byte for byte, with the earlier questions and prefill
|
|
blocks taken from the turn before. With two slots, two conversations taking
|
|
turns, also two with one long system prompt and two that ask the same clips,
|
|
give the bytes of sessions of their own and take at least what those take, and
|
|
a third one drops what was used least recently; with one slot and with none,
|
|
they give the same bytes. Without the option, four conversations taking turns
|
|
each take what they take alone, and a fifth drops the questions of the least
|
|
recently used. `lfm2_audio_backbone_test` checks that a chunked prefill
|
|
restored from the ones before gives a fresh runtime's bits, at 1, 4 and 8
|
|
threads and with caches of other lengths, and with several kept prompts:
|
|
prompts taking turns, the least recently used dropped, prompts that start the
|
|
same, one that parts inside a kept one, prompts of whole blocks, a shorter one
|
|
and one under a block.
|
|
|
|
### CPU
|
|
|
|
On the CPU, the quantized packages run the matmul kernels of ggml's CPU extra
|
|
buffer types where those take the weight's type and shape, as llama.cpp does
|
|
(`lfm2_audio.cpu_repack`, on by default). ggml's repacked kernels take Q4_0 on
|
|
x86 with AVX2, and Q4_0, Q8_0 and Q6_K on Arm. They cover the LFM2 layers of the
|
|
backbone and the detokenizer, the depthformer and the encoder's linears. The
|
|
depthformer's input projection, whose rows each codebook step reads apart, the
|
|
embedding tables, which rows are gathered from, and the few matrices whose shape
|
|
the kernels do not take keep the plain layout. The text head is tied to the
|
|
token embedding, so on Arm it gets a repacked copy of it: 105 MiB of Q6_K with
|
|
the Q4_0 packages, 136 MiB of Q8_0 with Q8_0. F16 and F32 weights, and Q8_0 on
|
|
x86, run the plain, non-repacked kernels, unless ggml has its AMX buffer
|
|
(below). ggml logs each weight it repacks on stderr as the model loads.
|
|
|
|
ggml builds its AMX buffer for x86 CPUs with AMX (Xeon Sapphire Rapids and
|
|
later) in a native build, and in the CPU variant for them that the release
|
|
builds carry. That buffer comes first and also takes the F16, Q8_0 and K-quant
|
|
matrices whose rows come in multiples of 32, so the F16 and Q8_0 packages run
|
|
other kernels there too, and the text head gets its copy with them as well,
|
|
256 MiB with F16. No CPU with AMX was measured. On a Xeon 8358 server CPU
|
|
(AVX-512, no AMX), a build like the portable release builds (`GGML_BACKEND_DL`
|
|
with all CPU variants), which loaded the icelake variant there, repacked the
|
|
same weights as a native build and gave its outputs byte for byte.
|
|
|
|
Real-time factor with the repacked kernels / without them
|
|
(`lfm2_audio.cpu_repack=false`) on an Apple M3 Ultra, the same binary at 16
|
|
threads, median of three runs that alternate the two: ASR over the
|
|
200-utterance sets as above, with the CLI, and TTS through `audiocpp_server` on
|
|
three sentences of 2.5 to 5 s:
|
|
|
|
| CPU | ASR EN Q4_0 | ASR EN Q8_0 | ASR JP Q4_0 | TTS EN Q4_0 | TTS EN Q8_0 |
|
|
|---|---|---|---|---|---|
|
|
| Apple M3 Ultra, 16 threads | 0.032 / 0.052 | 0.040 / 0.056 | 0.031 / 0.052 | 0.156 / 0.177 | 0.200 / 0.222 |
|
|
|
|
On x86, an Intel Core i9-13900HK laptop CPU (AVX2, no AVX-512 or AMX; 6
|
|
performance cores with two threads each and 8 efficiency cores) ran with the
|
|
repacked kernels / without them (main's code, whose outputs `cpu_repack=false`
|
|
reproduces byte for byte), median of four runs that alternate the two: with
|
|
the CLI, ASR of a 60 s clip and of ten short clips and TTS of a
|
|
three-sentence text with three seeds, and through `audiocpp_server` the first
|
|
audio of a reply on the S2S live route, measured as in its table above:
|
|
|
|
| Intel Core i9-13900HK | 8 threads | 4 threads |
|
|
|---|---|---|
|
|
| ASR EN Q4_0, 60 s clip | 0.121 / 0.187 | 0.132 / 0.183 |
|
|
| ASR EN Q4_0, short clips | 0.127 / 0.229 | 0.141 / 0.203 |
|
|
| ASR JP Q4_0, 60 s clip | 0.106 / 0.192 | 0.115 / 0.161 |
|
|
| ASR JP Q4_0, short clips | 0.120 / 0.234 | 0.136 / 0.198 |
|
|
| TTS EN Q4_0 | 0.359 / 0.437 | 0.356 / 0.427 |
|
|
| S2S EN Q4_0, first audio on the live route | 694 / 1494 ms | 775 / 1171 ms |
|
|
|
|
Q8_0 moves no weight there, and its ASR ran within about 2% of main's. With 12
|
|
or 20 threads, which put a third to a half of the running threads on the
|
|
efficiency cores (a fifth to a third at 8), each of these real-time
|
|
factors was higher than at the better of 4 and 8 threads, with the repacked
|
|
kernels and without.
|
|
|
|
The encoder and the prefill multiply many steps at a time, where the kernels
|
|
gain the most, so ASR gains more than speech, whose frames are a step each.
|
|
On the M3 Ultra, measured as in the streaming tables above, the first audio of
|
|
a TTS stream came after 48 ms with Q4_0 and 54 ms with Q8_0 (99 and 95 ms
|
|
without), and that of a reply on the S2S live route after 205 and 228 ms (358
|
|
and 342 ms). There llama.cpp's `llama-server`, on the CPU only at 16 threads,
|
|
transcribed the EN Q4_0 set at RTF 0.033 through its HTTP API, and at 0.043
|
|
with `--no-repack`, where audio.cpp's 0.032 is in process. Loading takes up to
|
|
0.7 s longer with the repacked kernels on the M3 Ultra, and at most 0.05 s
|
|
longer on the i9-13900HK.
|
|
|
|
The repacked kernels add up the same products in another order, so results move
|
|
a little and can change where two choices are close. With them, 195 of the 200
|
|
EN Q4_0 transcripts were byte for byte the ones without them on the M3 Ultra
|
|
and on the Xeon 8358 (both at 16 threads), and 193 on the i9-13900HK (at 8);
|
|
with JP Q4_0, 196, 196 and 195; with EN Q8_0 on the M3 Ultra, 196. WER over the
|
|
English set went from 1.48% to 1.51% (M3 Ultra), from 1.41% to 1.48% (Xeon)
|
|
and from 1.43% to 1.53% (i9-13900HK) with Q4_0, and from 1.43% to 1.46% with
|
|
Q8_0 (M3 Ultra); CER over the Japanese set went from 9.29% to 9.26%, from 9.21%
|
|
to 9.36% and from 9.34% to 9.31%. Speech and replies part from those of the
|
|
plain kernels within their first frames; a different thread count alone makes
|
|
them part after 0.3 to 0.8 s (M3 Ultra). Transcribed back, 40 sampled takes of
|
|
one sentence (seeds 1 to 40) came back exactly 40 times with them and 36 times
|
|
without (Xeon, Q4_0), 37 and 39 times (M3 Ultra, Q4_0) and 39 and 40 times (M3
|
|
Ultra, Q8_0). The first text token of the S2S reply to
|
|
`assets/resources/c.wav` is a near-tie, its two best logits 0.036 apart on the
|
|
M3 Ultra CPU with Q8_0 and 0.048 on Metal, and there the repacked kernels swap
|
|
them, so the greedy reply differs from its first word. The other CPU figures on
|
|
this page that count transcripts or replies were measured without the repacked
|
|
kernels.
|
|
|
|
### Memory
|
|
|
|
Memory is dominated by the weights, about the package size. Between chunks and
|
|
requests the encoder also keeps its graph and compute buffer, at most what a
|
|
30 s chunk needs: about 194 MiB, which took about 190 to 250 MiB of device
|
|
memory on CUDA, depending on the requests before. The buffer of a longer chunk
|
|
is freed after it. With the Q4_0 package on an Apple M3 Ultra, the server's
|
|
memory footprint on Metal, kept buffer included, was 1168 MB after a first 3.5 s
|
|
request and 1267 to 1310 MB over the next 59, alternating 70 s and 3.5 s of
|
|
audio. The Q4_0 files store the token embedding as Q6_K, which CUDA cannot
|
|
gather rows from, so on CUDA the backbone also keeps a 256 MiB F16 copy of it.
|
|
On the CPU, where ggml's extra buffers take the token embedding's type (Q6_K
|
|
and Q8_0 on Arm; with ggml's AMX buffer also F16), the backbone also keeps a
|
|
copy of it in their layout as the text head (see [CPU](#cpu)).
|
|
An S2S session also keeps the questions and prefill blocks of up to
|
|
`lfm2_audio.conversation_cache_slots` conversations (four by default) in host
|
|
memory, up to about 350 MiB each at the conversation limit; see
|
|
[Conversations](#conversations).
|
|
|
|
## Limitations
|
|
|
|
- The live route, `/v1/audio/speech/live`, answers each request as a first
|
|
turn; conversations run through `/v1/tasks/run` and `/v1/tasks/stream`
|
|
(which streams each reply with `"stream_format": "sse"`), the CLI's request
|
|
JSON or the C API.
|
|
- ASR is offline only; TTS and S2S also stream.
|
|
- TTS speaks with the built-in voices only; there is no voice cloning.
|
|
- On the CPU, F16 and F32 weights, and Q8_0 on x86, run ggml's generic
|
|
matmul kernels, as it has no repacked ones for them. Only its AMX buffer,
|
|
where built, takes F16 and Q8_0, and no CPU with AMX was measured (see
|
|
[CPU](#cpu)).
|