AI Pulse

Borealis:训练音频大语言模型的开源配方(数据、代码、权重全公开)

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

阅读原文
📚 相关主题 开源多模态

📬 订阅 AI Pulse

每天三次更新,不错过重要信号

▲ 回到顶部