rvq-codec / README.md
phanerozoic's picture
Card: standardized form with hero
f69318f verified
|
Raw
History Blame
5.44 kB
---
license: apache-2.0
library_name: kernels
tags:
- kernel
- audio
- codec
- quantization
- cpu
---
# rvq-codec
Residual vector quantization encode and decode for EnCodec, DAC, and
SNAC-class audio codecs and the audio language models built on them,
loadable through `kernels`. Decode output is bit-identical to the framework
result (`torch.equal`), not merely close. The reference baselines are three
framework formulations of the same operation, all measured.
Every audio language model ends at an RVQ decode: a stack of codebook
lookups summed into a waveform latent, run once per frame of generated
audio. Written in framework ops it is Q separate advanced-index gathers,
each materializing an `[N, D]` tensor, followed by Q-1 adds, and every one
of those intermediates exists only to be summed away. This kernel is a
single traversal that produces exactly the same bytes.
![Four-way decode bars per codec configuration: one-hot matmul, stack-and-sum, gather-and-sum, and the kernel](https://huggingface.co/kernels/phanerozoic/rvq-codec/resolve/main/media/hero.gif)
*Decode of 30 s of audio on 14 x86 threads, against three framework
formulations of the same operation: the one-hot matmul route at 17.5 to
141.7 ms, stack-and-sum at 1.63 to 8.45, a hand-written gather-and-sum at
0.48 to 2.20, and the kernel at 0.33 to 3.22, with output identical to all
of them.*
## Usage
```python
import kernels
rv = kernels.get_kernel("phanerozoic/rvq-codec", version=1, trust_remote_code=True)
q = rv.ResidualVQ(codebooks) # [Q, K, D], or a list of [K, D]
codes = q.encode(x) # [..., D] -> [..., Q] int32
recon = q.decode(codes) # [..., Q] -> [..., D]
recon = q.decode(codes, n_stages=4) # truncate for a lower bitrate
```
`version` selects the release branch; `trust_remote_code` is required by
`kernels` for publishers without the trusted-publisher mark.
## API
| Symbol | Purpose |
|---|---|
| `ResidualVQ(codebooks)` | plan holding the codebook stack and precomputed norms |
| `.encode(x, n_stages)` | residual quantization to `[..., Q]` int32 codes |
| `.decode(codes, n_stages)` | codes to `[..., D]`; truncate stages for lower bitrate |
## Method
The two directions have opposite structure, and the measurements say so.
Decode is a single traversal: Q codebook lookups accumulated in place, with
no intermediate per stage. It is exact, so the output is the framework
result to the bit.
Encode's cost is a dense `[N, D] x [D, K]` product per stage, which BLAS
already runs at hardware peak. A hand-blocked search measured 0.03-0.09x
against it, and transposing the codebook to remove the horizontal reductions
did not help, because the real constraint is codebook reuse across vectors,
which is exactly what a blocked GEMM provides. So the product stays in the
framework and only what follows it is fused: argmin, the gather, and the
residual subtract, which a framework implementation materializes twice. That
took the worst case from 4641 ms to 272 ms. Distances use
`||r - c||^2 = ||r||^2 - 2 r.c + ||c||^2`, dropping the term constant in k,
with codebook norms precomputed once by the plan.
## Measured
Decode of 30 s of audio, 14 threads, x86-64, against three framework
formulations of the same operation:
| config | rvq-codec | gather + sum | stack + sum | one-hot matmul |
|---|---:|---:|---:|---:|
| EnCodec Q=8 K=1024 D=128 | 0.33 ms | 0.48 ms | 1.78 ms | 17.5 ms |
| SNAC Q=4 K=4096 D=768 | 0.73 ms | 0.76 ms | 1.63 ms | 98.7 ms |
| Mimi Q=32 K=2048 D=256 | 3.22 ms | 2.20 ms | 8.45 ms | 141.7 ms |
The kernel is 44x to 136x the one-hot matmul formulation and 2.2x to 5.5x
the stack-and-sum route, while a carefully hand-written gather-and-sum loop
is at parity; the kernel's advantage there is that it needs no
intermediates and is one call rather than a loop.
Encode, same host, against the framework's matmul formulation:
| config | rvq-codec | cdist | matmul | dist tensor |
|---|---:|---:|---:|---:|
| EnCodec Q=8 K=1024 D=128 | 31.6 ms | 24.7 ms | 24.3 ms | 73.7 MB |
| DAC Q=9 K=1024 D=8 | 30.3 ms | 16.9 ms | 22.0 ms | 95.1 MB |
| SNAC Q=4 K=4096 D=768 | 70.5 ms | 67.9 ms | 66.0 ms | 59.0 MB |
| Mimi Q=32 K=2048 D=256 | 224.7 ms | 156.4 ms | 187.1 ms | 393.2 MB |
Encode runs at 0.73-0.94x of the framework matmul formulation while avoiding
the `[N, K]` distance materialization, which is 59 to 393 MB across the
stage loop at these sizes.
## Correctness
Decode output is `torch.equal` to the framework result at every
configuration measured, not merely close.
Reconstruction error is non-increasing in the number of stages only when
each stage's codebook scale tracks the residual it quantizes, which is what
training produces. With every stage at the same scale, stage 2 subtracts a
code far larger than stage 1's residual and error goes up. That is a
property of the codebooks, not of the algorithm, and the test suite pins
both behaviors.
## Requirements and limits
- CPU, `x86_64-linux` and `aarch64-linux`, for PyTorch 2.11, 2.12, and 2.13
(six prebuilt variants), each architecture built on its own host and
validated against the prebuilt binary.
- Encode is at parity with the framework matmul by design; the win there is
the removed distance materialization, not throughput.
## References
Zeghidour et al., SoundStream (2021); Défossez et al., EnCodec (2022);
Kumar et al., DAC (2023); Siuzdak et al., SNAC (2024).
## License
Apache-2.0.