Mimi (Kyutai 2024) β€” LiteRT on-device

On-device LiteRT conversion of kyutai/mimi, the Kyutai/Moshi streaming neural audio codec (24 kHz, 12.5 Hz frame rate). The heavy SEANet convolutional halves run on the CompiledModel GPU delegate (LITERT_CL); the two 8-layer Transformers and the split RVQ run on CPU. Device-verified on a Pixel 8a (Tensor G3): full round-trip at RTF β‰ˆ 0.35 (faster than real-time), reconstruction at the codec's quality floor.

Mimi β€” original vs 32-codebook RVQ round-trip on-device (LiteRT)

Files

File Size Delegate In β†’ Out
mimi_enc_conv_fp16.tflite 24 MB GPU audio [1,1,L] β†’ feat [1,512,Se]
mimi_enc_tx_fp16.tflite 50 MB CPU feat [1,Se,512] β†’ emb [1,512,Tc]
mimi_dec_tx_fp16.tflite 48 MB CPU emb [1,512,Tc] β†’ conv_in [1,512,seq]
mimi_deconly_fp16.tflite 28 MB GPU conv_in [1,512,seq] β†’ audio [1,1,L]
mimi_rvq.bin 69 MB CPU codes ↔ emb (32 codebooks, float32 LE)

Graphs are fixed-length (built per duration). The example set is for a 2 s clip (Se=50, Tc=25, seq=50).

Pipeline

audio β†’[GPU enc_conv]β†’ feat β†’[CPU enc_tx]β†’ emb β†’[CPU RVQ.encode]β†’ codes
      β†’[CPU RVQ.decode]β†’ emb β†’[CPU dec_tx]β†’ conv_in β†’[GPU deconly]β†’ audio

Minimal usage

Android (Kotlin, LiteRT CompiledModel)

fun load(name: String, acc: Accelerator) =                       // models staged in filesDir
    CompiledModel.create(File(filesDir, name).absolutePath, CompiledModel.Options(acc), null)

val encConv = load("mimi_enc_conv_fp16.tflite", Accelerator.GPU) // audio[1,1,48000] -> feat[1,512,50]
val encTx   = load("mimi_enc_tx_fp16.tflite",  Accelerator.CPU)  // feat.T[1,50,512] -> emb[1,512,25]
val decTx   = load("mimi_dec_tx_fp16.tflite",  Accelerator.CPU)  // emb[1,512,25] -> convIn[1,512,50]
val deconly = load("mimi_deconly_fp16.tflite", Accelerator.GPU)  // convIn -> audio[1,1,48000]

val inB = encConv.createInputBuffers(); val outB = encConv.createOutputBuffers()
inB[0].writeFloat(audio)     // 48000 floats = 2 s @ 24 kHz, [-1,1]
encConv.run(inB, outB)
val feat = outB[0].readFloat()  // transpose (1,512,50)->(1,50,512), feed encTx, then host RVQ.
// Full chain incl. the split-RVQ host code: compiled_model_api/audio_codec in litert-samples.

Python (desktop verification, full round-trip incl. RVQ codes)

import numpy as np, soundfile as sf
from ai_edge_litert.interpreter import Interpreter

def run(path, x):
    it = Interpreter(model_path=path); it.allocate_tensors()
    it.set_tensor(it.get_input_details()[0]["index"], x.astype(np.float32)); it.invoke()
    return it.get_tensor(it.get_output_details()[0]["index"])

wav, _ = sf.read("in.wav", dtype="float32")               # 24 kHz mono
x = np.zeros((1, 1, 48000), np.float32); n = min(len(wav), 48000); x[0, 0, :n] = wav[:n]

# 1) encode: SEANet convs (GPU graph) -> transformer + downsample -> emb [1,512,25]
feat = run("mimi_enc_conv_fp16.tflite", x)                # [1,512,50]
emb = run("mimi_enc_tx_fp16.tflite", feat.transpose(0, 2, 1))[0]   # [512,25]

# 2) split RVQ (host). mimi_rvq.bin = sem_Win[256,512], aco_Win[256,512], sem_Wout[512,256],
#    aco_Wout[512,256], sem_CB[2048,256], 31x aco_CB[2048,256] β€” float32 LE, contiguous.
D, S, H = 256, 2048, 512
w, o = np.fromfile("mimi_rvq.bin", "<f4"), 0
def take(*sh):
    global o; n = int(np.prod(sh)); a = w[o:o + n].reshape(sh); o += n; return a
sem_Win, aco_Win, sem_Wout, aco_Wout = take(D, H), take(D, H), take(H, D), take(H, D)
sem_CB, aco_CB = take(S, D), [take(S, D) for _ in range(31)]

def nearest(r_T, CB):                                     # Euclidean argmin over the codebook
    return ((CB * CB).sum(1)[None] - 2.0 * (r_T @ CB.T)).argmin(1)

codes = np.zeros((32, emb.shape[1]), np.int64)            # 1 semantic + 31 acoustic @ 12.5 Hz
codes[0] = nearest((sem_Win @ emb).T, sem_CB)
res = aco_Win @ emb                                       # acoustic residual loop
for i in range(31):
    codes[1 + i] = nearest(res.T, aco_CB[i]); res -= aco_CB[i][codes[1 + i]].T

# 3) decode: codes -> emb -> transformer + upsample -> SEANet deconv (GPU graph)
q = sem_Wout @ sem_CB[codes[0]].T + aco_Wout @ sum(aco_CB[i][codes[1 + i]] for i in range(31)).T
conv_in = run("mimi_dec_tx_fp16.tflite", q[None])         # [1,512,50]
sf.write("roundtrip.wav", run("mimi_deconly_fp16.tflite", conv_in)[0, 0], 24000)

Why hybrid

Every op in all four graphs is GPU-clean (re-authored), and the convs are fp16-exact on Mali (decoder-only fed the exact transformer output = 48 dB SNR). But the decoder transformer's residual stream reaches |x|=27, where the GPU delegate's internal fp16 compute loses precision β€” full-GPU decode drops to ~12 dB on real speech. The transformer behaves identically standalone and fused on device, so this is fp16 precision, not a fusion artifact. The transformers are tiny (8 layers Γ— 512, seq ~50), so CPU is trivial and exact; the heavy SEANet convs stay on GPU. The split RVQ (Euclidean argmin + int64 indices) runs on CPU.

Re-authoring (litert-torch, parity ~1.0)

tanh-GELU Β· baked RoPE cos/sin + rotate_half Β· baked causal additive bias Β· MimiLayerScaleβ†’Linear Β· grouped ZeroStuffConvT1d (depthwise upsample, no TRANSPOSE_CONV) Β· baked constant conv pad Β· nn.ELUβ†’relu(x)βˆ’relu(1βˆ’exp(min(x,0))) Β· replicate-padβ†’SLICE+CONCAT.

Sample app

A complete Android sample app + the conversion/RVQ-export scripts are in the official LiteRT samples repository under compiled_model_api/audio_codec (google-ai-edge/litert-samples). Push these files to the app's filesDir with that sample's install_to_device.sh.

License follows upstream Mimi (CC-BY-4.0).

Performance

Measured on a Pixel 8a (Tensor G3, Android 16) with the standard TFLite benchmark_model tool β€” 10 warm-up runs then 50 timed runs, reported as the tool's mean.

Runtime Backend Graph on GPU Latency
TFLite benchmark_model (TfLiteGpuDelegateV2) β€” mimi_dec_tx_fp16.tflite GPU (OpenCL) 689 / 689 21.7 ms
TFLite benchmark_model (TfLiteGpuDelegateV2) β€” mimi_deconly_fp16.tflite GPU (OpenCL) 220 / 220 114.3 ms
TFLite benchmark_model (TfLiteGpuDelegateV2) β€” mimi_enc_conv_fp16.tflite GPU (OpenCL) 189 / 189 93.4 ms
TFLite benchmark_model (TfLiteGpuDelegateV2) β€” mimi_enc_tx_fp16.tflite GPU (OpenCL) 681 / 681 28.0 ms
TFLite benchmark_model β€” mimi_dec_tx_fp16.tflite CPU (XNNPACK, 4 threads) β€” XNNPACK declined the graph
TFLite benchmark_model β€” mimi_deconly_fp16.tflite CPU (XNNPACK, 4 threads) β€” 936.2 ms
TFLite benchmark_model β€” mimi_enc_conv_fp16.tflite CPU (XNNPACK, 4 threads) β€” XNNPACK declined the graph
TFLite benchmark_model β€” mimi_enc_tx_fp16.tflite CPU (XNNPACK, 4 threads) β€” 53.1 ms

Any on-device figure recorded when this model shipped came from a different runtime. It was taken through LiteRT's own CompiledModel accelerator (logcat reports it as LITERT_CL), which is the path the Kotlin sample app and the LiteRT API use, and it appears elsewhere on this card. The rows above are the classic TFLite OpenCL delegate, measured with a tool anyone can download and re-run. The two are not comparable, so read the rows above as a reproducible floor rather than as this model's speed on LiteRT.

XNNPACK declines these fp16 graphs β€” it reports failed to delegate DEPTHWISE_CONV_2D and then fails to allocate tensors β€” so there is no usable CPU number. Disabling XNNPACK falls back to reference kernels, which measured about 20Γ— slower than the GPU on models of this size and would not represent CPU inference anyone would ship.

Downloads last month
65
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for litert-community/Mimi

Base model

kyutai/mimi
Finetuned
(7)
this model

Collection including litert-community/Mimi