![【Bug已解决】[Bug]: Crash on Transcription (size for tensor a must match the size of tensor b) with repro](http://pic.xiahunao.cn/yaotu/【Bug已解决】[Bug]: Crash on Transcription (size for tensor a must match the size of tensor b) with repro)
【Bug已解决】[Bug] Crash on Transcription (size for tensor a must match the size of tensor b) with reproduce 解决方案一、现象长什么样用语音转录Transcription模型处理音频时崩溃RuntimeError: The size of tensor a (1500) must match the size of tensor b (1499) at non-singleton dimension 1或者更简略size for tensor a must match the size of tensor b几个特征只在**转录音频输入**时炸文本对话正常——说明问题在音频分支。不是每次都炸短音频 / 特定长度音频正常某些长度的音频必炸。这暗示和音频时长 / 特征长度有关。崩在「把位置编码 / mask 加到音频特征上」或「cross-attention 拼接」那一步。报错里两个 size 往往差 1如 1500 vs 1499典型的「长度公式差一个 off-by-one」。本质音频特征提取器CNN实际输出的时间步数和模型「以为」的时间步数位置编码表 / 注意力 mask 的长度对不上于是特征 位置编码或cat时形状不匹配。二、背景语音转录模型Whisper 类 / 多模态语音模型的流程是特征提取把波形变成 mel 谱图形状(n_mels, time)time取决于音频时长。CNN 下采样用卷积把time压成time // stride或类似得到音频编码器输入长度设为T_actual。位置编码模型有一张注册好的位置编码表pos_emb长度T_expected加在音频特征上。编码器 / cross-attention音频特征与文本侧交互。问题出在第 2、3 步的长度对齐。模型计算T_expected时用的是「理想公式」T_expected time // stride # 假设完美整除但 CNN 实际输出长度受kernel 大小 padding影响真实公式是T_actual (time - kernel_size) // stride 1 # 或含 padding 的变体当time不是stride的干净倍数时T_expected和T_actual差 1。于是位置编码表长度 T_expected如 1500实际 CNN 输出长度 T_actual如 1499两者做audio_feat pos_embbroadcast 加法时在dim1上 1500 ≠ 1499 → 报size for tensor a must match the size of tensor b。为什么「某些长度才炸」因为只有当time不能被 stride 整除、且 kernel 偏移导致 off-by-one 时两个长度才不等整除的音频如恰好 30 秒两个长度相等不炸。这就是「时灵时不灵」的根源。三、根因根因是音频特征 CNN 的实际输出长度与位置编码表 / mask 的「预期长度」用了不同的计算公式导致非整除时长下 off-by-one 形状不匹配三层第一层主因长度公式不一致。模型用T_expected time // stride分配位置编码和 mask但 CNN 实际输出是(time - kernel)//stride 1。两式在time % stride ! 0时差 1。这是最经典的「预估长度 vs 实测长度」脱节。第二层位置编码表长度写死不随实际张量长度适配。pos_emb注册成固定长度T_expected。理想做法是用pos_emb[:T_actual]按实际长度切片或做长度插值。但代码直接audio_feat pos_emb假设两者必然等长一旦不等就崩。第三层没有在入口处对齐音频长度。特征提取前应先把timepad / 裁剪到 stride 的整数倍或到 CNN 公式能整除的值从源头保证T_actual T_expected。但这一步被漏了把对齐责任甩给了后面「假设等长」的加法。一句话CNN 实测长度与位置编码预期长度公式不一致非整除音频时长下 off-by-one加位置时形状不匹配崩溃。四、最小可运行复现下面用纯 Pythonnumpy 风格模拟「CNN 实测长度 vs 位置编码预期长度 off-by-one导致相加崩溃」的控制流不需要 GPUdef cnn_output_len(time, kernel, stride): # 真实 CNN 输出长度含 kernel 偏移 return (time - kernel) // stride 1 def expected_len(time, stride): # 模型「理想」预估漏了 kernel 偏移 return time // stride def add_positional_buggy(audio_feat_len, pos_emb_len): # 模拟 audio_feat pos_emb 的广播加法 if audio_feat_len ! pos_emb_len: raise RuntimeError( fsize for tensor a ({audio_feat_len}) must match ftensor b ({pos_emb_len}) at dim 1 ) return True def main(): kernel, stride 3, 2 # 一段非整除时长的音频time3000, stride2 - 预期 1500 time 3000 t_actual cnn_output_len(time, kernel, stride) # (3000-3)//21 1499 t_expected expected_len(time, stride) # 3000//2 1500 print(CNN 实测长度:, t_actual, 位置编码预期:, t_expected) try: add_positional_buggy(t_actual, t_expected) except RuntimeError as e: print(复现成功:, e) if __name__ __main__: main()跑出来会打印CNN 实测长度: 1499 位置编码预期: 1500然后复现成功: size for tensor a (1499) must match tensor b (1500) at dim 1和线上「差 1 的形状不匹配」完全一致。五、解决方案第一层最小直接修复最省事的救火在特征提取前把音频时长 pad / 裁剪到 CNN 公式能整除的长度保证T_actual T_expecteddef align_audio_length(time: int, kernel: int, stride: int) - int: # 反推一个能让 cnn_output_len expected_len 的 time # 简单做法把 time 调整到 (kernel-1) 模 stride 同余使两式相等 # 更直接pad 到 stride 整数倍 补偿 kernel 偏移 target ((time - (kernel - 1) stride - 1) // stride) * stride (kernel - 1) return target # 用法特征提取前先对齐 time raw_waveform.shape[-1] time_aligned align_audio_length(time, kernel3, stride2) waveform pad_waveform(raw_waveform, time_aligned)或者更简单粗暴让位置编码按实际长度切片而不是写死# 原来audio_feat pos_emb 假设等长崩 # 改成audio_feat pos_emb[: audio_feat.size(1)] 按实际切片永不 mismatch pos self.pos_emb[: audio_feat.size(1)] audio_feat audio_feat pos这一行改动就能让「非整除时长」不再崩代价是位置编码只用前T_actual个语义正确。六、解决方案第二层结构性改进第一层是「对齐或切片」第二层是「让长度成为单一事实来源永远以 CNN 实测输出长度为准位置编码 / mask 都按它适配」从设计上消灭公式分歧import torch class AudioLengthPolicy: def __init__(self, kernel: int, stride: int): self.kernel kernel self.stride stride def cnn_len(self, time: int) - int: # 唯一事实来源和 CNN 实现完全一致的输出长度公式 return (time - self.kernel) // self.stride 1 def align_input(self, time: int) - int: # 反推一个使下游无需切片的输入长度可选用于性能最优 # 让 cnn_len(time_aligned) 尽量接近 time//stride 且两者一致 # 这里简单地 pad 到让 (time - kernel) 被 stride 整除 rem (time - self.kernel) % self.stride if rem ! 0: time (self.stride - rem) return time class PositionalAdder: def __init__(self, policy: AudioLengthPolicy, max_pos: int): self.policy policy self.pos_emb torch.randn(max_pos, 768) def add(self, audio_feat: torch.Tensor) - torch.Tensor: T_actual audio_feat.size(1) # 关键位置编码按实测长度切片不假设固定长度 assert T_actual self.pos_emb.size(0), 超出位置编码表上限 return audio_feat self.pos_emb[:T_actual] def build_audio_pipeline(kernel3, stride2, max_pos5000): policy AudioLengthPolicy(kernel, stride) adder PositionalAdder(policy, max_pos) return policy, adder这样policy.cnn_len是长度的唯一权威公式CNN、位置编码、mask 都引用它。位置编码永远按audio_feat.size(1)切片绝不等长假设。若想零开销可在入口align_input把时长 pad 到整除值下游连切片都不用。七、解决方案第三层断言 / CI 守护把「实测长度 位置编码长度」「非整除时长不崩」「公式一致」固化成测试import torch import pytest def test_cnn_len_matches_expected_on_divisible(): p AudioLengthPolicy(kernel3, stride2) # time 让 (time-3) 被 2 整除time3003 assert p.cnn_len(3003) 3003 // 2 def test_pos_adder_no_mismatch(): p, adder build_audio_pipeline() feat torch.randn(1, 1499, 768) # 非整除产生的 1499 out adder.add(feat) # 不应 RuntimeError assert out.shape (1, 1499, 768) def test_pos_adder_raises_when_over_max(): p, adder build_audio_pipeline(max_pos100) feat torch.randn(1, 200, 768) with pytest.raises(AssertionError): adder.add(feat) def test_align_input_removes_off_by_one(): p AudioLengthPolicy(kernel3, stride2) t p.align_input(3000) # 对齐后 cnn_len 应等于 t//2无 off-by-one assert p.cnn_len(t) t // 2 def test_transcription_various_lengths(): p, adder build_audio_pipeline() for time in [1000, 1999, 2001, 3000, 3001]: t p.align_input(time) feat torch.randn(1, p.cnn_len(t), 768) out adder.add(feat) # 各种长度都不应崩 assert out.size(1) p.cnn_len(t)再加一个端到端回归随机时长音频转录不崩def test_transcribe_random_durations(): model make_transcription_model() for dur in [5, 11, 23, 37, 60]: audio make_audio(secondsdur) # 不同长度 out model.transcribe(audio) # 不应 size mismatch assert out is not None八、排查清单看报错是不是size for tensor a (N) must match tensor b (M)且崩在音频分支特征位置编码 / cat→ 坐实本问题。是否「某些音频长度才炸、文本正常」→ 是长度公式 off-by-one。临时救火位置编码按实际长度切片pos_emb[:T_actual]或 pad 音频到整除长度。检查 CNN 输出长度公式与位置编码/mask 的「预期长度」公式是否一致最常见差kernel-1。长期修复长度单一事实来源引用同一cnn_len位置编码按实测切片入口对齐音频长度。升级模型实现到合了音频长度对齐修复的版本并跑上面的「各种时长」回归。若差的不只 1差很多可能是n_mels或 batch 维对不上先确认是dim1时间还是别的维。九、小结转录时size for tensor a must match tensor b不是音频数据坏了而是音频 CNN 实测输出长度与位置编码/mask 的预期长度用了不同公式非整除时长下 off-by-one相加/拼接时形状不匹配。最小修复是位置编码按实际长度切片或 pad 音频到整除长度结构性修复是让长度成为单一事实来源、位置编码永远按实测切片、入口对齐最后用 pytest 把「实测预期」「各种时长不崩」「公式一致」锁死。抓住「张量长度必须以实测为准、预分配表必须按实测切片/插值」这条所有音频/视频特征长度不匹配的坑都能照此排查。