Borealis:训练音频大语言模型的开源配方(数据、代码、权重全公开)
Borealis — open data, code, weights recipe for training Audio LLM
Open 5B audio-language model for Russian and English. Open source, open data, full recipe to reproduce.
By Ilya · Ksenia · Nikolay · Konstantin · Alexander — VikhrModels
嘿。Borealis 已经默默酝酿了大约一年——这是我们对 Voxtral / Flamingo-audio 的开源实现。今天我想分享我们如何从头训练它,哪些有效,哪些无效。 配方本身没有特别新颖的地方:Whisper3-large,Qwen 4B 作为 LLM 骨干,中间粘一个适配器。
为什么做音频-LLM
经典 ASR(Whisper, Wav2Vec2)转录得好但不懂语义。问 Whisper“这段音频讲的是什么?”你只会得到一份转录文本。音频-LLM 填补了这个鸿沟——它们既能听到又能推理。 我们训练 Borealis 的目标:
- 总结长录音 - 回答关于内容的问题 - 推理语气和情绪
架构
配方是成熟的:一个强大的音频编码器,一个强大的 LLM,以及它们之间的适配器。
`
Audio @ 16 kHz [输入]
│ waveform → log-mel
▼
Whisper Large V3 encoder [冻结]
│ 1280维 · 约1500 tokens / 30秒 · 635M参数
▼
4× 下采样 + MLP 适配器 [训练]
│ 拼接4帧 → 5120 → 2560 · 约375 tokens / 30秒
▼
Qwen3-4B · 因果 LLM [LoRA微调]
│
▼
文本回复 [输出]
`
为什么选这套组件
- Whisper Large V3 — 最佳开源语音编码器,尤其多语言。 - 冻结编码器 — 保持 ASR 质量。基本上所有 VLM 都这样做——我们实际上只是在训练一个 VLM,只不过针对音频。 - 4× 下采样 — 1500 → 375 tokens。音频不是密集信道,压缩是值得的。 - Qwen3-4B — 用我们手头有的。
总共约5B参数,其中约500M被训练——LLM上的LoRA加上适配器。
数据集
我们为消融实验组装了几个数据池。
所有八个数据集都在一处 → Borealis 训练数据集集合
注意。AudioBooksInstructGemini2.5 是通过将有声读物分块并利用 Gemini 2.5 Pro 生成指令构建的——包括摘要、问答、分析、结构化输出。生成脚本是开源的。
实验设置
我们的问题:
- 我们需要解冻某些层吗? - 训练数据的语言影响有多大——俄语 vs 英语? - 添加纯文本指令有帮助吗? - 语言和文本数据的最佳比例是多少?
配置:基础检查点 AlexWortega/Borealis5b_90k · 8× GPU · batch 1 / GPU · grad-accum 16(有效128)· LR 1e-5 · WER 在6个俄语基准上。
结果
#### 01 · 俄语 vs 英语
训练数据的语言影响有多大?
- 仅英语在俄语基准上达到20.88% WER——仅比原生俄语差1.5个百分点。
这表明有很强的跨语言迁移:
- Whisper 已经懂得俄语(多语言预训练)。 - Qwen3 也懂俄语。 - 适配器只需要在一种语言中对齐;其余部分会迁移。
不过——原生数据仍然更好,而且令人惊讶的是,将英语混入俄语反而使情况变得更糟。不要为了“多样性”而稀释目标语言数据。
#### 02 · 添加纯文本指令
混合纯文本有帮助吗?
非线性:
- 10% 文本 → 小幅改进(19.32 → 19.17)。 - 25% 文本 → 退化(→ 24.02)。
在25%时,模型开始忘记音频任务——LLM 漂移到文本到文本模式,不再正确拟合音频嵌入。 💡 要点:10–15% 文本有帮助;25% 有害。存在明显的甜区。
#### 03 · 网络研讨会问题
有一个基准始终表现很差。
我们所有的运行在网络研讨会上大约有60% WER,而纯 Whisper 只有 7.77%。 网络研讨会 = 噪音、回声、破麦克风、小众行话、多人发言和打断。Whisper 编码器能处理所有这些,但 LLM 会“过度校正”转录文本,使其更符合语法——结果就很糟糕。
所有运行
[... 表格省略,因原文只列了框架]
幕后——服务与集成到 transformers
服务多模态模型并不是新话题。快速部分(编码器)+慢速部分(LLM):异步运行编码器,累积 logits,然后交给 LLM。我们将音频分块用于 Whisper 并按原样服务。本节其余部分是关于修补 vLLM 的平淡故事。
#### A · 适配器——简单 vs 深度
borealis/modeling.py 提供了两个适配器。生产中使用简单的——一个2层 MLP,无偏置:
`python
class AudioLanguageAdapter(nn.Module):
# ~31M 参数,用于 Whisper-large × Qwen3-4B
def __init__(self, hidden_size: int, dim: int):
super().__init__()
self.w_in = nn.Linear(hidden_size, dim, bias=False)
self.gelu = nn.GELU()
self.w_out = nn.Linear(dim, dim, bias=False)
def forward(self, x):
return self.w_out(self.gelu(self.w_in(x)))
`
维度:
- encoder.d_model = 1280 · downsample_factor = 4 → hidden_size = 5120 - llm.config.hidden_size = 2560 = dim - 网络:Linear(5120, 2560) → GELU → Linear(2560, 2560) - 参数:5120·2560 + 2560·2560 ≈ 19.7M 矩阵;加上缓冲区约31M
一个更重的 AudioLanguageAdapterDeep(约80M 参数)也存在于仓库中——三个类似 transformer 的块,带有 LayerNorm + GELU + 残差 + dropout。没有投入生产;简单的 MLP 就足够了。 4× 下采样是一个简单的视图——四个相邻帧拼接成一个,通道维数变为4倍:
`python
def _downsample(self, seq):
k = self.downsample_factor # 4
T, d = seq.shape # 1500 × 1280
target = k * math.ceil(T / k)
if target != T:
seq = F.pad(seq, (0, 0, 0, target - T))
return seq.contiguous().view(target // k, d * k) # 375 × 5120
`
Token 流:1500 × 1280(Whisper输出)→ 375 × 5120(下采样)→ 375 × 2560(适配器)。这375个嵌入填充了 <|AUDIO|> 占位 token 的位置。 编码器被硬冻结:encoder.eval(),然后 for p in encoder.parameters(): p.requires_grad = False。
#### B · 音频增强
borealis/augmentations.py 是一个课程机器:一个 AugmentationPipeline,包含十几个随机效果,外加一个 AugmentationScheduler 回调,在不同 epoch 激活不同阶段。从干净开始,随时间越来越严酷。 管道中包含什么(每个都由自己的 p 控制):
- 背景噪音混合 — SNR 18–28 dB · 咖啡馆、街道、空调嗡嗡声 - IR 卷积 — 房间和厅堂混响 - EQ — ±6 dB · 不同麦克风曲线 - 随机增益 — ±3 dB - 带通 — 150–350 / 3200–5200 Hz · 廉价麦克风 - 重采样 — 14–20 kHz · 低带宽信道 - 电话 — 8–12 kHz · 180–4200 Hz · 电话、呼叫中心 - 编解码 — 96–160 kbps · MP3 / Opus 压缩 - 削波 — 0.82–0.95 · 过驱动信号 - 音高/速度 — ±4 半音 · 0.8–1.2× - SpecAugment — ≤2 频率掩码(27 bins),≤2 时间掩码(100 帧)
AugmentationScheduler 是一个 HF TrainerCallback;在 on_epoch_begin 时,它根据 start_epoch 选择当前的 AugmentationStage。课程:先干净音频,后来逐渐更严酷的失真。
#### 听一听:处理前后对比
干净片段来自 ToneBooks;噪声采样自 Vikhrmodels/Audio_Noise_Dataset 的 Musan 部分。混合是模型在训练时启用噪声增强后看到的。
“然而他们一点也不讨人喜欢——恰恰相反,他们令人震惊和恐惧。”
① 干净样本 ToneBooks ② 仅噪声 Musan ③ 语音+噪声 SNR ~10 dB ④ 电话 300–3400 Hz · 8 kHz
第三个样本是模型在严格课程时期训练的:文本几乎在可理解性的边缘,但正是这阻止了适配器“粘”在干净的 Whisper 信号上。第四个是经典电话频带(300–3400 Hz,通过8 kHz 重采样往返)。
#### C · 修补 vLLM
有趣的部分。vLLM 开箱即用只支持一组封闭的多模态架构(Qwen2-Audio, LLaVA, Phi-4-MM 等)。Borealis 不在其中——Whisper 编码器 + 自定义适配器 + Qwen3 + 两个额外词汇 token。为了获得加速,我们编写了一个 vLLM 插件。 插件(vllm_borealis)位于 HF 模型仓库中权重旁边。两个文件:
- __init__.py — 入口点。向 vllm.ModelRegistry 注册模型。 - borealis.py — 约 400 行,四个类用于 vLLM API。
`python
def register():
from vllm import ModelRegistry
if "BorealisForConditionalGeneration" not in ModelRegistry.get_supported_archs():
ModelRegistry.register_model(
"BorealisForConditionalGeneration",
"vllm_borealis.borealis:BorealisForConditionalGeneration",
)
`
vLLM 通过 pyproject.toml 中的入口点(组 vllm.general_plugins)拾取 register()。从那时起,config.json 中的 "BorealisForConditionalGeneration" 就是一个一等架构名称——就像原生的一样。 vLLM 期望的四个类:
- BorealisProcessingInfo — 声明模态。关键行:get_supported_mm_limits() == {"audio": 1} 强制每个提示一个音频。还公开了 WhisperFeatureExtractor 用于波形到 mel。 - BorealisDummyInputsBuilder — 为预热和分析合成 30 秒空音频,以便 vLLM 可以调整 KV-cache 大小。 - BorealisMultiModalProcessor — 神奇类。当用户编写一个带有 <|AUDIO|> 的提示时,处理器将其扩展为 <|start_of_audio|> + 375×<|AUDIO|> + <|start_of_audio|>,并通过 PromptUpdateDetails.select_token_id(..., embed_token_id=audio_token_id) 将这 375 个 token 标记为“嵌入将外部提供”。 - BorealisForConditionalGeneration — 模型本身。持有 WhisperEncoder、我们的 AudioLanguageAdapter,以及——最好的部分——init_vllm_registered_model(architectures=["Qwen3ForCausalLM"]) 而不是重新实现的 LLM。
关键技巧。我们从未为 vLLM 重新实现 Qwen3。我们告诉 vLLM “给我们你自己的优化 Qwen3 块”(通过 init_vllm_registered_model),然后免费获得 paged-attention、continuous batching 和 fused kernels。我们唯一拥有的是音频输入和适配器:Whisper → downsample → adapter。
`python
from vllm.model_executor.models.utils import (
init_vllm_registered_model, maybe_prefix,
)
llm_config = AutoConfig.from_pretrained("Qwen/Qwen3-4B") llm_config.vocab_size = 151671 # base 151669 + 2 audio tokens
self.llm = init_vllm_registered_model(
vllm_config=vllm_config,
hf_config=llm_config,
prefix=maybe_prefix(prefix, "llm"),
architectures=["Qwen3ForCausalLM"], # vLLM's own optimized impl
)
`
Token 级的魔法。vLLM 中多模态推理的难点在于将外部计算的嵌入(适配器输出)拼接到特定的 token 位置,同时不丢失任何其他优化。vLLM 通过 PromptReplacement + PromptUpdateDetails.select_token_id 处理:
`python
def get_replacement_borealis(item_idx):
# 30s audio → 1500 mel frames / 4 = 375 audio tokens
num_features = audio_embeds[item_idx].shape[0] # or 375 default
audio_tokens = [audio_token_id] * num_features
return PromptUpdateDetails.select_token_id(
[audio_marker_id] + audio_tokens + [audio_marker_id],
embed_token_id=audio_token_id, # ← “这些 token 携带外部嵌入”
)
return [PromptReplacement(
modality="audio",
target="<|AUDIO|>", # user prompt 中的单个占位符
replacement=get_replacement_borealis,
)]
`
提示中的一个 <|AUDIO|> 膨胀为 377 个 token(标记 + 375 + 标记)。375 个“真实”音频 token 通过 embed_token_id 获得适配器嵌入;其他一切通过正常的 LLM 嵌入表流动。 一些底层细节:
- 词汇表调整。Qwen3 base = 151669。我们添加 <|AUDIO|>(id 151669)和 <|start_of_audio|>(id 151670) → vocab_size = 151671。如果 config.json 中没有这些精确 id,插件会回退到它们。 - 多余的批次维度。vLLM 有时将 mel 作为 [N, 1, 128, 3000] 传递,因为它将多模态字段打包到自己的表中。插件用 if input_features.dim() == 4 and shape[1] == 1: squeeze(1) 保护。经典陷阱。 - merge_by_field_config = True — 告诉 vLLM 在合并请求时自动批处理多模态字段。否则你需要手动编写 collator。 - 音频只计算一次。编码器 + 适配器每次 generate 调用运行一次;得到的 375 个嵌入像普通 token 一样存在于 KV-cache 中。每个后续的 next-token 步骤只触及 LLM——因此音频前端成本在长生成过程中被摊平。
2.1 倍加速从何而来。比较在一个方向上是不公平的:原生 transformers 使用 eager attention 和动态分配,没有连续批处理。vLLM 增加了:
- PagedAttention — KV-cache 存在于页表中;没有 GPU 分钟浪费在填充/碎片化上。 - Continuous batching — 变长请求不必等待批次中最慢的那个。 - Fused Qwen3 kernels — 针对 attention 和 MLP 优化的 CUDA 内核,尤其在 bf16 下。
在 NVIDIA A100 上测量,30 秒音频,max_tokens=128,bf16。原生 transformers:44.9 tok/s。vLLM 插件:95.9 tok/s。当 batch ≥4 时差距进一步拉大。 完整插件源代码(约 400 行):Vikhrmodels/Borealis-5b-it/tree/main/vllm_borealis
实用建议
- 始终从预训练开始。没有预训练,模型不会在合理时间内收敛。没有检查点?先预训练纯 ASR。 - 从原生数据开始。跨语言迁移有效,但原生数据更好。对于俄语——收集俄语音频。 - 添加文本——但只加一点点。10–15% 的纯文本指令有帮助。25% 会倒退。 - 不要混合音频语言。俄语 + 英语音频没有胜过纯俄语。语言会竞争容量。 - 为嘈杂音频规划独立路径。对于会议或呼叫中心——单独微调或回退到 Whisper。一个通用检查点无法覆盖两者。
局限
- 音频超过约 30 秒——调用者必须分块。 - 重噪声——WER 退化。 - 流式——目前仅离线。 - 多音频提示——限制为 1。
如何尝试
通过 transformers 的最小推理:
`python
from transformers import AutoModel
import torchaudio
model = AutoModel.from_pretrained( "Vikhrmodels/Borealis-5b-it", trust_remote_code=True, device="cuda", )
audio, sr = torchaudio.load("audio.wav") if sr != 16000: audio = torchaudio.functional.resample(audio, sr, 16000)
output = model.generate(
audio=audio.squeeze(),
user_prompt="What is this audio about? <|start_of_audio|><|end_of_audio|>",
system_prompt="You are a helpful voice assistant.",
max_new_tokens=256,
)
print(model.decode(output[0]))
`
对于生产环境,我们推荐 vLLM(2倍更快):
`bash
pip install vllm>=0.12.0
vllm serve Vikhrmodels/Borealis-5b-it \
--trust-remote-code \
--dtype bfloat16
`
为俄语训练音频-LLM 是可行的,但需要细致考虑。一句话总结:预训练至关重要,原生数据胜出,少量文本有帮助,噪声本身是个问题。Borealis 并不完美——但它是一个坚实的俄语音频-LLM 工作开源基线,我们希望这篇文章能节省别人几百个 GPU 小时。
链接
- 🤗 模型 — Vikhrmodels/Borealis-5b-it - 💻 代码 — github.com/VikhrModels/Borealis - 🎙 演示 — Vikhrmodels/Borealis-inference - 📊 数据集集合 — Borealis 训练数据集 - 📰 完整交互式文章 — AlexWortega/borealis-blog - 𝕏 作者 — @justALEXWORTEGA
引用
@misc{borealis2025, title = {Borealis: Audio-Language Model for Speech Understanding}, author = {VikhrModels}, year = {2025}, url = {https://huggingface.co/Vikhrmodels/Borealis-5b-it} }
© 2026 VikhrModels · Apache 2.0