Prhub

#53829 [Bugfix][Model] Fix CohereASR streaming audio-token estimate (unit + subsampling)

原始 PR 作者 hungnnvidia 合并时间 2026-09-02 23:55 文件变更 2 提交数 2 评论 27 代码增减 +43 / -3

执行摘要

修复 CohereASR 流式转录使用统计中音频令牌估算的单位错误和子采样遗漏,导致 prompt_tokens 被夸大 5 个数量级。

根据 PR body 描述,此修复针对流式转录使用统计(streaming transcription usage stats)的严重计量错误。原始实现 hop_length = window_stride(约 0.01)被误用作 audio_duration_s * sample_rate(样本数)的除数,导致计算出的音频令牌数被夸大约 5 个数量级(例如 1 秒音频估算为 1,600,000 个令牌,而预期约 13 个)。这直接影响用户对资源使用情况的正确评估。

这是一个质量很高的 bugfix PR,强烈建议精读。核心变更是一个小范围但至关重要的计算逻辑修复,直接解决了影响用户可见统计的严重错误。讨论中关于 ceil/floor 对齐、数据源一致性和错误处理风格的交流非常有价值,体现了对代码健壮性和一致性的关注。测试设计(使用具体数值断言)也值得借鉴。

讨论亮点

讨论集中在几个实现细节的权衡上:

  1. ceil vs. floor:aaron-seq 指出原始实现使用 ceil 计算帧数,而 get_seq_len 使用 floor,导致约 11% 的时长会多报一个令牌。作者随后修正为 //(floor 除法)。
  2. 数据源一致性:aaron-seq 建议从 preprocessor 字典读取 sample_rate,而非 stt_config.sample_rate,以与 get_hf_processor 对齐并避免未来不同步。作者采纳了此建议。
  3. 错误处理风格:讨论了 window_stride 和 subsampling_factor 是使用带默认值的 .get() 还是直接索引。
    - aaron-seq 和 ekagra-ranjan 建议使用直接索引,让配置错误更早暴露(KeyError)。
    - hungnnvidia 在初版对 window_stride 使用了 .get("window_stride", 0.01),但最终根据反馈改为直接索引(preprocessor["window_stride"])。
    - 对于 subsampling_factor,作者坚持使用直接索引(hf_config.encoder["subsampling_factor"]),认为设置默认值(如 1)会静默导致估算值乘以 8,而抛出 KeyError 是更安全的选择,并指出 per-request 路径 CohereASRProcessingInfo.get_num_audio_tokens 也使用直接索引。
  4. 测试策略:aaron-seq 指出初始测试可能基于与代码相同的公式推导预期值,无法真正检测回归。最终测试断言了具体的数值结果。
  5. 文件命名:DarkLight1337 建议简化测试文件名,从 test_cohere_asr_audio_tokens.py 改为 test_cohere_asr.py,作者执行了此变更。

实现拆解

  1. 核心逻辑修复:修改 vllm/model_executor/models/cohere_asr.py 中的 CohereAsrForConditionalGeneration.get_num_audio_tokens 类方法。
    - 单位修正:不再将 preprocessor["window_stride"](秒)直接用作 hop_length。而是从预处理器字典读取 sample_rate 和 window_stride,计算 hop_length = int(window_stride * sample_rate),得到正确的样本跨度(160 samples)。
    - 计算对齐:将帧数计算从 math.ceil(duration * sample_rate / hop_length) 改为 int(audio_duration_s * sample_rate) // hop_length(floor 除法),以对齐 get_seq_len 路径。
    - 补充子采样:新增除以 hf_config.encoder["subsampling_factor"](值为 8),然后取 math.ceil 得到最终令牌数。
    - 数据源统一:sample_rate 和 window_stride 均从 model_config.hf_config.preprocessor 字典直接索引读取,与 get_hf_processor 保持一致,避免了从 stt_config 读取可能的不同步问题。
  2. 测试覆盖:新增测试文件 tests/models/multimodal/test_cohere_asr.py。
    - 使用参数化测试验证不同音频时长(1.0s, 10.0s, 30.0s)下的估算结果。
    - 断言具体的、已知正确的令牌数(13, 125, 375),而非重新推导公式,有效防止回归。
    - 构造了包含 preprocessor 和 encoder 字典的 model_config 模拟对象。
文件 模块 状态 重要度
vllm/model_executor/models/cohere_asr.py 模型执行器 modified 6.3
tests/models/multimodal/test_cohere_asr.py 模型测试 added 6.32

关键符号

CohereAsrForConditionalGeneration.get_num_audio_tokens

关键源码片段

vllm/model_executor/models/cohere_asr.py core-logic

包含核心 bugfix,修改了 `get_num_audio_tokens` 类方法,修复了流式转录使用统计的令牌估算逻辑。

# 修复后的 CohereASR.get_num_audio_tokens 类方法
@classmethod
def get_num_audio_tokens(
    cls,
    audio_duration_s: float,
    stt_config: SpeechToTextConfig,
    model_config: ModelConfig,
) -> int | None:
    hf_config = model_config.hf_config
    preprocessor = hf_config.preprocessor
    # 从预处理器字典读取 sample_rate 和 window_stride,与 get_hf_processor 保持一致
    sample_rate = preprocessor["sample_rate"]
    window_stride = preprocessor["window_stride"]
    # 将 window_stride(秒)转换为样本跨度(hop_length),单位为 samples
    hop_length = int(window_stride * sample_rate)
    # 使用 floor 除法计算帧数,与 get_seq_len 路径对齐
    num_frames = int(audio_duration_s * sample_rate) // hop_length
    # 除以编码器子采样因子,得到最终的令牌数估算
    subsampling_factor = hf_config.encoder["subsampling_factor"]
    return math.ceil(num_frames / subsampling_factor)

评论区精华

帧数计算的取整策略(ceil vs floor) 正确性

reviewer `aaron-seq` 指出原始实现使用 `ceil` 计算帧数,而 per-request 路径 `get_seq_len` 使用 `floor`,导致约 11% 的非 hop 倍数时长会多报一个令牌。作者回复表示已修正为 `//`(floor 除法)以对齐。 > reviewer: `get_seq_len floors here (floor_divide, and exact_pad is off by default), this ceils. I measured a one token difference on about 11% of durations. Can we use // so the two paths actually agree?` > author: `Fixed — switched the frame count to // (floor) to match get_seq_len's floor_divide (with exact_pad off by default), so the two paths agree and the ~11% one-token-high cases are gone.`

结论:采用 floor 除法,与 `get_seq_len` 保持一致,消除了不一致性。 · 已解决

sample_rate 和 window_stride 的数据来源 设计

reviewer `aaron-seq` 建议从 `preprocessor` 字典读取 `sample_rate`,而非 `stt_config.sample_rate`,以与 `get_hf_processor` 保持一致并避免未来不同步。作者采纳了此建议。 > reviewer: `get_hf_processor builds its hop from preprocessor["sample_rate"], this one uses stt_config.sample_rate. Same value today only because get_speech_to_text_config asserts 16000. I would read the same key.` > author: `Done — now reads preprocessor["sample_rate"] (same key as get_hf_processor) instead of stt_config.sample_rate, so it no longer relies on the 16000 assert being in sync.`

结论:统一从 `model_config.hf_config.preprocessor` 读取,增强了代码健壮性。 · 已解决

配置键的错误处理风格(默认值 vs 直接索引) 设计

讨论了 `window_stride` 和 `subsampling_factor` 的访问方式。 - ** 初始版本 **:作者对 `window_stride` 使用了 `.get("window_stride", 0.01)` 带默认值。 - ** 反馈 **:`aaron-seq` 和 `ekagra-ranjan` 建议使用直接索引,让配置错误更早暴露(KeyError),并避免在多处维护默认值。 - ** 最终决策 **:作者在最终版本中对两者都采用了直接索引(`preprocessor["window_stride"]` 和 `encoder["subsampling_factor"]`)。 > reviewer (`ekagra-ranjan`): `Can we drop the guard so any config issue gets surfaced up and we dont have to maintain defaults in multiple places in the code?` > author: `Dropped the guards — sample_rate and window_stride are now read via direct indexing (preprocessor["sample_rate"] / preprocessor["window_stride"]), so a malformed config surfaces as a loud KeyError and we dont duplicate defaults.` > author (on `subsampling_factor`): `Kept encoder["subsampling_factor"] as a direct lookup on purpose... A default like 1 would silently 8x the estimate on a malformed config, which felt worse than a loud error.`

结论:采用直接索引访问配置键,优先让配置错误快速失败(fail-fast),而非提供可能误导的默认值。 · 已解决

测试用例的预期值来源 测试

reviewer `aaron-seq` 指出初始测试可能基于与代码相同的公式推导预期值,无法真正检测回归。要求使用具体数值进行断言。 > reviewer: `expected is built from the same formula as the code under test, so this passes whatever the rounding does. Could we assert the 13 / 125 / 375 from the description, or compare against CohereASRProcessingInfo.get_num_audio_tokens?` > author: `Fixed — the test now asserts concrete pinned values (13 / 125 / 375) instead of re-deriving them from the same formula, so a rounding regression is actually caught.`

结论:测试断言了已知的具体数值结果,增强了测试的防护能力。 · 已解决

测试文件命名简化 style

maintainer `DarkLight1337` 建议简化测试文件名。 > reviewer: `Let's simplify this file to just test_cohere_asr.py` > author: `Done @DarkLight1337 — renamed the test file to tests/models/multimodal/test_cohere_asr.py.`

结论:采纳建议,重命名测试文件以保持简洁。 · 已解决

风险与影响

  1. 逻辑风险(低):修复逻辑清晰,对齐了已验证的 per-request 路径(get_seq_len + subsampling_factor)。主要风险在于修改了流式使用统计的计算逻辑,如果实现有误,会影响 prompt_tokens 计数。
  2. 兼容性风险(低):变更仅影响 CohereASR 模型的流式转录使用统计计算,不改变模型推理的核心计算或输出。
  3. 回归风险(低):新增了针对此核心计算路径的单元测试,且使用了具体数值进行断言,能有效防止回归。测试覆盖了不同时长(包括非 hop 倍数时长)。
  4. 配置依赖风险(中):修复后的代码直接索引 hf_config.preprocessor 和 hf_config.encoder 字典。如果模型配置缺少这些键,会抛出 KeyError。这是一种有意的设计选择,旨在让配置问题更早暴露,但需要确保目标模型(如 CohereLabs/cohere-transcribe-03-2026)的配置始终包含这些字段。

用户影响:修复了 CohereASR 模型在流式转录场景下向用户报告的 prompt_tokens 使用统计的严重错误(夸大 ~5 个数量级)。这有助于用户正确理解资源消耗。
系统影响:仅影响 CohereASR 模型的特定计算路径(get_num_audio_tokens 类方法),不涉及核心调度、内核或分布式通信逻辑。变更范围小且隔离。
团队影响:此修复是一个高质量的 bugfix PR,展示了清晰的根因分析、与 reviewer 的有效沟通和代码改进。它也为多模态模型类似的使用统计计算提供了一个良好的实践范例。

配置依赖

关联 Issue

#39259 Fix Cohere ASR failing to load when librosa is not installed

完整报告

参与讨论