diffusion-asr / README.md
Tachyeon's picture
Reconcile gap and width statements with the sweep findings
ab6821a verified
|
Raw
History Blame Contribute Delete
8.65 kB
---
language: en
license: other
base_model:
- JetLM/SDAR-1.7B-Chat
- Qwen/Qwen3-ASR-0.6B
datasets:
- librispeech_asr
tags:
- automatic-speech-recognition
- speech
- diffusion
- lora
- block-diffusion
pipeline_tag: automatic-speech-recognition
---
# Diffusion ASR
Speech recognition with a **block-diffusion language model as the decoder**. A frozen audio
encoder and a frozen text diffusion LM are joined by a small trained adapter plus LoRA β€” about
1 % of the stack is trained.
**4.60 % WER on LibriSpeech test-clean** (all 2,620 clips).
> **These are trained deltas, not a standalone model.** This repo holds the 5.25 M-parameter
> adapter and the 17.4 M-parameter LoRA. Running them also needs the two frozen base models
> below and the inference code from
> [ipritamdash/diffusion-asr-research](https://github.com/ipritamdash/diffusion-asr-research).
## Why this exists
The audio encoder came from a working ASR system. That system's own decoder is thrown away here
and a text-only diffusion model put in its place, connected through roughly 5 M trained
parameters.
The question is whether a language model that has never seen audio can be conditioned on
acoustic evidence through a thin learned interface β€” and whether writing several tokens per
forward pass buys anything.
Compare it against other frozen-encoder + frozen-LM + adapter systems. Do not compare it against
end-to-end ASR trained on thousands of hours; it saw 30.
## Results
Full LibriSpeech test-clean, 2,620 clips, corpus WER after Whisper English normalisation.
| Decoder | Config | WER | vs real time |
|---|---|---:|---:|
| **Reference loop** | block 2 | **4.60** | β€” |
| Reference loop | block 4 | 5.23 | β€” |
| **LMDeploy engine, swept config** | **block 4 Β· 4 steps Β· `sequential`** | **4.78** | 7.0Γ— |
| LMDeploy engine, old default | block 2 Β· dynamic | 5.00 | 9.1Γ— |
| LMDeploy engine, old default | block 4 Β· dynamic | 5.63 | 13.5Γ— |
Speed is single-stream real-time multiple on an A100; batched serving measured far higher
(~800Γ— on one L40S in an earlier benchmark). Under the engine's original confidence-ordered
commit rule it read a constant 0.40 worse than the reference loop; the swept `sequential`
commit order closes half of that (engine best 4.78 vs reference 4.60). The remaining +0.18 is
unexplained and stated rather than left out.
### Error analysis
Computed over every clip of the 4.60 decode.
**Output health** β€” zero empty transcripts, zero repeat loops, zero length blow-ups, and no clip
hit the token budget.
**Composition** β€” of 53,029 reference words: 3.75 % substituted, 0.40 % deleted, 0.45 %
inserted. Four errors in five are a *wrong* word rather than a missing one; the model commits to
a plausible word from unclear evidence instead of failing to produce one.
**Substitutions** β€” 40 % are within a small character edit of the reference (*jailer* β†’
*jailor*, *holmes* β†’ *homes*), which reads as blurred acoustic detail rather than a language
failure.
**Multi-piece words** β€” words the tokeniser splits into several subword pieces are 7.5 % of
words but carry 52 % of errors, at 13Γ— the failure rate of single-piece words. This is the
dominant failure mode.
**Length and position** β€” the shortest quarter of clips is hardest (5.85 % against 4.37 % on the
longest), and quality is flat across an utterance. A fixed token budget and a block decoder could
have degraded on long input; neither did.
## Files
| File | Contents | Params |
|---|---|---:|
| `adapter.pt` | audio adapter + `gain`, CTC logit scale, CTC blank projection | 5,250,049 (+2,050) |
| `lora.pt` | LoRA on the decoder, PEFT state dict, 392 tensors | 17,432,576 |
`adapter.pt` keys: `adapter`, `log_s`, `blank_w`, `blank_b`, `epoch`, `adapter_kind` (`conv`),
`adapter_stride` (`1`), `attn_mode` (`stair`), plus dev metrics.
`lora.pt` keys: `lora`, `epoch`, `dev_ce`, `objective`.
`log_s`, `blank_w` and `blank_b` belong to the CTC alignment loss used during training. They are
not used at decode time, and are kept so training can be resumed or audited.
## Base models β€” frozen, download separately
| Role | Repo | Pinned revision |
|---|---|---|
| Decoder | `JetLM/SDAR-1.7B-Chat` | `97cf0dc9a7e85433f3f71a6bbac6fec9d381294f` |
| Encoder | `Qwen/Qwen3-ASR-0.6B` | `5eb144179a02acc5e5ba31e748d22b0cf3e303b0` |
Use these exact revisions β€” the weights here were trained against them.
> The decoder must be loaded through the project repo's patched loader
> (`qwen_sdar/sdar.py::load_patched_sdar`), which swaps CUDA-only flash-attn for SDPA and adds
> the embeddings-input path. Plain `from_pretrained` will not reproduce these numbers.
## Architecture
```
audio 16 kHz β†’ Qwen3-ASR-0.6B encoder β†’ adapter β†’ SDAR-1.7B decoder + LoRA β†’ text
frozen, 1024-d @ 12.5 Hz trained frozen trained
```
**Adapter** β€” `Conv1d(1024β†’1024, k=3, stride=1)` β†’ GELU β†’ `Linear(1024β†’2048)` β†’
`LayerNorm(2048)` β†’ Γ—`gain`. Stride 1 keeps the encoder's native 12.5 Hz; pooling to 6.25 Hz
measured worse across three seeds per arm, at roughly half the training cost.
`gain` initialises to the RMS of the decoder's token embeddings. A random projection emits
vectors 10–50Γ— the size of real word vectors, and a decoder fed a prefix that loud learns to
ignore it; matching the embedding scale is what makes the audio legible at all.
**LoRA** β€” `r=16`, `alpha=32`, dropout 0, on `q,k,v,o,gate,up,down` in all 28 layers. LoRA scales
updates by `alpha/r`, so the effective learning rate on those weights is 2Γ— nominal.
**Attention is block-causal at every stage**, training and inference alike. Each block sees the
audio, all earlier blocks, and itself. Training and serving therefore use the same convention,
and the model runs on a standard diffusion inference engine without retraining.
## Training
30 hours of LibriSpeech `train-clean-100`, speaker-disjoint from the held-out split. Three
stages, never joint:
| Stage | Trains | Loss |
|---|---|---|
| Grounding | adapter | CTC only, decoder detached |
| SFT | adapter | CTC + diffusion cross-entropy |
| LoRA | LoRA | diffusion cross-entropy only, adapter frozen |
Roughly 32 GPU-hours in total.
## Decoding
Block-causal, blocks written left to right, earlier blocks frozen once committed.
| | |
|---|---|
| Block width | 4 (engine, swept) Β· 2 (reference loop) |
| Denoising steps per block | up to 4 |
| Commit rule | reference loop: confidence β‰₯ 0.95 with forced fallback; engine: **`sequential` unmasking** (position order), found best in a 50-arm sweep |
| Token budget | 160 |
A 50-arm sweep of every engine dial (widths 1–64, steps, thresholds, all three unmasking
strategies), confirmed on the full dev set, chose the engine config above; decoding wider than
the trained block-4 width collapses (width 8 already mass-produces empty output), so the
frontier honestly ends at width 4.
With the original confidence-ordered commits, width 2 (below the paper's swept range) measured
best; with `sequential` commits the trained width 4 wins β€” commit order, not width alone, was
carrying that result.
## Usage
The encoder and decoder have conflicting dependencies, so inference runs in two steps in two
environments. Both need a CUDA GPU.
```bash
git clone https://github.com/ipritamdash/diffusion-asr-research
cd diffusion-asr-research
huggingface-cli download Tachyeon/diffusion-asr adapter.pt lora.pt --local-dir .
# 1. encoder environment: audio -> features
pip install -r requirements-encoder.txt
python featurize.py --audio clip.wav --out features.pt
# 2. decoder environment: features -> transcript
pip install -r requirements-decode.txt
python decode.py --features features.pt --adapter adapter.pt --lora lora.pt --block_length 2
```
`decode.py` reads `adapter_stride` from the checkpoint and refuses to run if it is not recorded.
A stride-1 and a stride-2 adapter have identical tensor shapes, so guessing wrong loads cleanly
and silently emits a prefix of the wrong length.
For serving, merge the LoRA and run on LMDeploy's PyTorch diffusion engine; see `engine/` in the
project repo.
## Scope
A research model for **English read speech**. LibriSpeech is clean, read audiobook material and
these numbers describe that domain.
## Licence
The adapter and LoRA here are trained deltas. Any use depends on the two frozen base models,
which carry **their own licences** β€” check `JetLM/SDAR-1.7B-Chat` and `Qwen/Qwen3-ASR-0.6B`
before redistributing anything, and do not redistribute a merged checkpoint (which embeds the
base weights) without confirming their terms.