docs: README 补全新建/重建流程 + 缓存删除边界 + GPU 利用率优化

- 新增「新建/重建容器」子节:首次新建、重建镜像(改代码)、改配置、改依赖
  四种场景的明确操作,区分何时需要重建镜像、何时只需重启
- 新增「缓存分层与删除边界」:BuildKit/pip/模型/运行时数据四类缓存的删除
  影响对照表 + 何时主动清缓存的指引
- 记录之前踩的缓存失效根因:docker builder prune --filter until=Nm 会清掉
  稳定 base 层(CACHED 跳过的层访问时间不刷新 → 被误判可回收)。明确禁止
  使用 until filter,给出正确替代写法
- 新增「GPU 利用率优化」章节:faster-whisper 尖刺波成因(CPU Mel 特征提取
  与 GPU 解码未重叠)+ batch_size 16→32 / beam_size 5→2 的参数选择依据 +
  为什么不能关 word_timestamps(segmenter 强依赖)+ 验证方法
- 配置差异表从 6 项补到 8 项(加 asr.batch_size / asr.beam_size),修正
  translation.batch_size 16→32
- asr/translation 字段表补齐 batch_size / beam_size / sort_by_length 行
- 配置示例同步更新(加 beam_size、sort_by_length,修正 batch_size)
This commit is contained in:
audio2text dev
2026-07-06 22:35:33 +08:00
parent a6b5c7231c
commit 4625650fc8

225
README.md
View File

@@ -21,6 +21,10 @@
- [断句与时间戳重算原理](#断句与时间戳重算原理) - [断句与时间戳重算原理](#断句与时间戳重算原理)
- [模型不共驻(显存策略)](#模型不共驻显存策略) - [模型不共驻(显存策略)](#模型不共驻显存策略)
- [Docker 说明](#docker-说明) - [Docker 说明](#docker-说明)
- [新建 / 重建容器](#新建--重建容器)
- [缓存分层与删除边界](#缓存分层与删除边界)
- [⚠️ 不要用 `docker builder prune --filter until`](#-不要用-docker-builder-prune---filter-until)
- [GPU 利用率优化](#gpu-利用率优化)
- [依赖](#依赖) - [依赖](#依赖)
- [常见问题](#常见问题) - [常见问题](#常见问题)
@@ -235,7 +239,7 @@ docker compose --profile cpu up -d --build # CPU
| ASR | `large-v3-turbo` | ~3GBFP16 | 8x 速度,质量接近 large-v3 | | ASR | `large-v3-turbo` | ~3GBFP16 | 8x 速度,质量接近 large-v3 |
| 翻译 | `facebook/nllb-200-distilled-1.3B` | ~2.5GBFP16 | 质量最好的蒸馏版 | | 翻译 | `facebook/nllb-200-distilled-1.3B` | ~2.5GBFP16 | 质量最好的蒸馏版 |
ASR 与翻译**不共驻**:翻译阶段先卸载 Whisper 释放显存,独占跑大 batch`batch_size=16` ASR 与翻译**不共驻**:翻译阶段先卸载 Whisper 释放显存,独占跑大 batch`batch_size=32`
两者峰值显存互不叠加,远低于 24G 上限。模型缓存(`./models` volume跨容器复用 两者峰值显存互不叠加,远低于 24G 上限。模型缓存(`./models` volume跨容器复用
CPU→GPU 切换时 NLLB/Whisper 大模型首次下载、之后秒起。 CPU→GPU 切换时 NLLB/Whisper 大模型首次下载、之后秒起。
@@ -249,16 +253,18 @@ AUDIO2TEXT_VARIANT=cpu ./setup.sh # 切回 CPU构建 cpu 镜像 + config.cp
./start.sh # 重新启动 ./start.sh # 重新启动
``` ```
两套配置的差异仅在 6 项(其余字段完全一致): 两套配置的差异仅在 8 项(其余字段完全一致):
| 字段 | CPU`config.cpu.yaml` | GPU`config.gpu.yaml` | | 字段 | CPU`config.cpu.yaml` | GPU`config.gpu.yaml` |
|---|---|---| |---|---|---|
| `asr.model` | `tiny.en` | `large-v3-turbo` | | `asr.model` | `tiny.en` | `large-v3-turbo` |
| `asr.device` | `cpu` | `cuda` | | `asr.device` | `cpu` | `cuda` |
| `asr.compute_type` | `int8` | `float16` | | `asr.compute_type` | `int8` | `float16` |
| `asr.batch_size` | `8` | `32` |
| `asr.beam_size` | `5` | `2` |
| `translation.model` | `Helsinki-NLP/opus-mt-en-zh` | `facebook/nllb-200-distilled-1.3B` | | `translation.model` | `Helsinki-NLP/opus-mt-en-zh` | `facebook/nllb-200-distilled-1.3B` |
| `translation.device` | `cpu` | `cuda` | | `translation.device` | `cpu` | `cuda` |
| `translation.batch_size` | `8` | `16` | | `translation.batch_size` | `8` | `32` |
### 启动后的入口 ### 启动后的入口
@@ -292,16 +298,18 @@ AUDIO2TEXT_VARIANT=cpu ./setup.sh # 切回 CPU构建 cpu 镜像 + config.cp
### CPU / GPU 两份配置的差异 ### CPU / GPU 两份配置的差异
其余字段存储、断句、日志、docs两份配置完全一致仅以下 6 项不同: 其余字段存储、断句、日志、docs两份配置完全一致仅以下 8 项不同:
| 字段 | `config.cpu.yaml` | `config.gpu.yaml` | | 字段 | `config.cpu.yaml` | `config.gpu.yaml` |
|---|---|---| |---|---|---|
| `asr.model` | `tiny.en` | `large-v3-turbo` | | `asr.model` | `tiny.en` | `large-v3-turbo` |
| `asr.device` | `cpu` | `cuda` | | `asr.device` | `cpu` | `cuda` |
| `asr.compute_type` | `int8` | `float16` | | `asr.compute_type` | `int8` | `float16` |
| `asr.batch_size` | `8` | `32` |
| `asr.beam_size` | `5` | `2` |
| `translation.model` | `Helsinki-NLP/opus-mt-en-zh` | `facebook/nllb-200-distilled-1.3B` | | `translation.model` | `Helsinki-NLP/opus-mt-en-zh` | `facebook/nllb-200-distilled-1.3B` |
| `translation.device` | `cpu` | `cuda` | | `translation.device` | `cpu` | `cuda` |
| `translation.batch_size` | `8` | `16` | | `translation.batch_size` | `8` | `32` |
### 完整字段 ### 完整字段
@@ -342,6 +350,8 @@ AUDIO2TEXT_VARIANT=cpu ./setup.sh # 切回 CPU构建 cpu 镜像 + config.cp
| `language` | str | `en` | 识别语言,仅英语 | | `language` | str | `en` | 识别语言,仅英语 |
| `word_timestamps` | bool | `true` | 词级时间戳:让断句精确(取首末词时间戳)而非纯匀速估算。建议开 | | `word_timestamps` | bool | `true` | 词级时间戳:让断句精确(取首末词时间戳)而非纯匀速估算。建议开 |
| `vad_filter` | bool | `true` | 过滤静音段,提升识别质量与速度 | | `vad_filter` | bool | `true` | 过滤静音段,提升识别质量与速度 |
| `batch_size` | int | `8`CPU/ `32`GPU | `BatchedInferencePipeline` 批量解码的音频块数。GPU 拉大 batch 拉长单次 GPU 解码时间,掩盖 CPU 提取 Mel 特征的间隙,提升平均利用率 |
| `beam_size` | int | `5`CPU/ `2`GPU | beam search 宽度。GPU turbo 模型鲁棒,降到 2 减少解码候选数与步数加速明显、质量损失小CPU 无加速诉求保持默认 5 |
#### `translation` — 翻译NLLB-200 #### `translation` — 翻译NLLB-200
@@ -351,8 +361,9 @@ AUDIO2TEXT_VARIANT=cpu ./setup.sh # 切回 CPU构建 cpu 镜像 + config.cp
| `device` | str | `cpu` | `cpu``cuda` | | `device` | str | `cpu` | `cpu``cuda` |
| `src_lang` | str | `eng_Latn` | NLLB 语言码:英语 | | `src_lang` | str | `eng_Latn` | NLLB 语言码:英语 |
| `tgt_lang` | str | `zho_Hans` | NLLB 语言码:简体中文 | | `tgt_lang` | str | `zho_Hans` | NLLB 语言码:简体中文 |
| `batch_size` | int | `16` | 翻译批量大小。不与 ASR 共驻时显存独占,可用大 batch | | `batch_size` | int | `8`CPU/ `32`GPU | 翻译批量大小。不与 ASR 共驻时显存独占,可用大 batch |
| `max_length` | int | `256` | 单条翻译最大 token 数 | | `max_length` | int | `256` | 单条翻译最大 token 数 |
| `sort_by_length` | bool | `true` | 按句子长度排序后分批,减少批内 padding 浪费GPU 收益大) |
#### `segmentation` — 断句与字幕规范化 #### `segmentation` — 断句与字幕规范化
@@ -419,14 +430,17 @@ asr:
language: en language: en
word_timestamps: true word_timestamps: true
vad_filter: true vad_filter: true
batch_size: 8 # GPU: 32(拉长单次 GPU 解码,掩盖 CPU 特征提取间隙)
beam_size: 5 # GPU: 2turbo 鲁棒可降,候选数↓解码步数↓)
translation: translation:
model: facebook/nllb-200-distilled-1.3B model: facebook/nllb-200-distilled-1.3B
device: cpu # GPU: cuda device: cpu # GPU: cuda
src_lang: eng_Latn src_lang: eng_Latn
tgt_lang: zho_Hans tgt_lang: zho_Hans
batch_size: 16 batch_size: 8 # GPU: 32(显存独占可用大 batch
max_length: 256 max_length: 256
sort_by_length: true # 按长度排序分批,减少 padding 浪费
segmentation: segmentation:
max_words_per_line: 14 max_words_per_line: 14
@@ -592,6 +606,51 @@ ASR 与翻译模型**不会同时驻留 GPU**。`model_manager.py` 单例跟踪
--- ---
## GPU 利用率优化
faster-whisper 的 GPU 利用率曲线常呈**尖刺波**(峰=批量解码满载,谷=CPU 提取 Mel 特征
+ 处理结果时 GPU 空闲),平均利用率偏低。瓶颈不在 GPU 算力,而在 CPU 特征提取与 GPU
解码未重叠:
```
CPU: [VAD+切片+Mel特征 N个chunk] ──► [处理结果] ──► [VAD+切片+Mel特征] ──► ...
GPU: (空闲) [批量解码] (空闲) [批量解码]
```
`BatchedInferencePipeline` 内部把音频按 30s chunk 切分,凑够 `batch_size` 个 chunk 一次性
送 GPU 解码。每批解码完后回到 CPU 处理结果 + 提取下一批 Mel 特征,这期间 GPU 空闲。
### 已做的优化GPU 配置)
| 参数 | 旧值 | 新值 | 作用 |
|---|---|---|---|
| `asr.batch_size` | 16 | **32** | 单次 GPU 解码时长翻倍CPU 特征提取间隙占比减半 → 尖刺变宽、谷底变浅平均利用率上升。turbo FP16 仅 ~1.6GB3090 24G 充裕 |
| `asr.beam_size` | 5 | **2** | 解码候选数 5→2每步计算量与解码步数下降 → 峰更密、间隙更短。turbo 鲁棒,保留 1 个候选做歧义发音保险,质量损失小 |
### 为什么不关 `word_timestamps`
`segmenter.py` 强依赖词级时间戳做精确断句——只要任一 segment 没词级时间戳,就整体退化
到匀速估算路(时间戳按字符数比例估算),字幕精度下降明显。所以 `word_timestamps=true`
必须保留,即使它是 CPU↔GPU 同步开销的来源之一。
### 验证方法
```bash
# 1. 确认配置生效
curl -s http://127.0.0.1:8001/health | python -m json.tool
# 应见 asr_batch_size=32, asr_beam_size=2
# 2. 跑长视频(如 test/1-5.mp4观察 GPU 利用率曲线
nvidia-smi dmon -s u # 实时 GPU 利用率d=dec u=util
# 3. 对比字幕质量(可选):同一视频改前改后 SRT diff
```
优化后尖刺应比之前密且谷底变浅,平均利用率上升。`beam_size=2` 对 turbo 模型质量损失
极小,但仍建议用同一视频 A/B 对比字幕确认无歧义发音处的降级。
---
## Docker 说明 ## Docker 说明
### 一份 Dockerfile两个镜像 ### 一份 Dockerfile两个镜像
@@ -606,25 +665,151 @@ ASR 与翻译模型**不会同时驻留 GPU**。`model_manager.py` 单例跟踪
两个镜像的 Python 依赖列表(`requirements.txt`)完全一致,仅 torch 不同。镜像内 apt 装 两个镜像的 Python 依赖列表(`requirements.txt`)完全一致,仅 torch 不同。镜像内 apt 装
`ffmpeg` + `patchelf` `ffmpeg` + `patchelf`
### Volume 挂载 ### 新建 / 重建容器
| 容器路径 | 宿主路径 | 用途 | 项目提供 `setup.sh` / `start.sh` / `stop.sh` 包装脚本,也可直接用 `docker` / `docker compose`
|---|---|---|
| `/data` | `./data` | 上传视频、中间音频、输出字幕、SQLite 数据库 |
| `/models` | `./models` | 模型缓存HF + ctranslate2跨容器复用避免重下 |
| `/app/config.yaml` | `./config.yaml` | 配置文件(只读挂载) |
镜像本身无状态、无敏感数据。 #### 首次新建(新机器 / 全新拉取代码后)
### docker-compose
`docker-compose.yml` 提供 `audio2text-cpu` / `audio2text-gpu` 两个 profile
```bash ```bash
docker compose --profile cpu up -d # CPU # 1. 构建镜像 + 生成 config.yamlCPU 默认)
docker compose --profile gpu up -d # GPU需 nvidia runtime ./setup.sh
# GPUAUDIO2TEXT_VARIANT=gpu ./setup.sh
# 2. 启动容器
./start.sh
# GPUstart.sh 检测到 audio2text:gpu 镜像 + nvidia-smi 自动加 --gpus all
``` ```
`setup.sh` 做三件事:检查 docker → 构建 `audio2text:{variant}` 镜像 → 把
`config.{variant}.yaml` 复制为 `config.yaml`(运行时实际读取的文件)。
#### 重建镜像(改了 app 代码或 requirements 后)
依赖层apt + pip + torch由 BuildKit 缓存挂载复用,只有 `COPY app` 层重建,通常
30 秒内完成。**重建不会动运行时数据**`./data` / `./models` 是挂载的 volume
```bash
# CPU直接重跑 setup.sh幂等会复用缓存层
./setup.sh
# 或显式构建:
docker build --build-arg VARIANT=cpu -t audio2text:cpu .
# GPU
AUDIO2TEXT_VARIANT=gpu ./setup.sh
# 或:
docker build --build-arg VARIANT=gpu -t audio2text:gpu .
# 重建后重启容器(替换运行中的旧镜像):
./stop.sh && ./start.sh
```
#### 改配置(不重建镜像)
`config.yaml` 是只读挂载,改完重启容器即生效,**无需重建镜像**
```bash
cp config.gpu.yaml config.yaml # 切换配置(或直接编辑 config.yaml
./stop.sh && ./start.sh
```
#### 改依赖requirements.txt / torch 版本)
会触发 deps 层重建,耗时较长(重装 torch + 全部依赖CPU ~3 分钟GPU ~5 分钟)。
BuildKit 的 pip 缓存挂载(`/root/.cache/pip`)跨构建复用已下载的 wheel二次构建会快
很多。
```bash
# 编辑 requirements.txt 后
./setup.sh # 或 docker build --build-arg VARIANT=gpu -t audio2text:gpu .
./stop.sh && ./start.sh
```
#### docker compose替代脚本
```bash
docker compose --profile dev up -d --build # 开发:源码挂载 + uvicorn reload改代码零重建
docker compose --profile cpu up -d --build # CPU 生产
docker compose --profile gpu up -d --build # GPU 生产(需 nvidia runtime
```
### 缓存分层与删除边界
这套构建涉及三类缓存,**删除策略截然不同**,乱删会导致全量重建:
| 缓存类型 | 位置 | 存什么 | 能删吗 | 删了会怎样 |
|---|---|---|---|---|
| **BuildKit 构建缓存** | Docker 内部(`docker builder` 管理) | Dockerfile 各层base / deps / final的构建产物 | ⚠️ 谨慎,见下方 | 命中失效 → 该层及下游全量重建 |
| **pip wheel 缓存** | BuildKit cache mount `/root/.cache/pip` | 下载过的 `.whl` 文件 | ✅ 可删 | 下次构建重新下载 wheel不重编译 |
| **模型缓存** | `./models` volume容器内 `/models` | Whisper / NLLB 权重HF + ctranslate2 | ✅ 可删 | 下次启动重新下载模型(~5.5GB GPU |
| **运行时数据** | `./data` volume容器内 `/data` | 上传视频 / 中间音频 / 输出字幕 / SQLite | ⚠️ 视情况 | 删了任务历史和产物全没 |
### ⚠️ 不要用 `docker builder prune --filter until`
**这是踩过的坑**。BuildKit 的 `--filter "until=30m"`(或任意时长)会清除"最近 N 分钟未
访问"的缓存层。问题在于:**稳定的基础层**(如 `base-gpu` 的 apt 装 python3.12)只在
首次构建时执行一次,之后每次构建都直接 CACHED 跳过——它的"最后访问时间"一直停在首次
构建那一刻,永远不会更新。于是 `--filter "until=..."` 会把这些**仍然在用的稳定层**当成
"很久没访问"清掉,导致下一次构建从 base 层开始全量重来GPU 镜像 ~10 分钟 + 重新下载
torch ~2.5GB)。
正确做法:
```bash
# ✅ 想清理磁盘、释放 BuildKit 缓存:用不带 filter 的 prune清全部未引用缓存
docker builder prune -f
# 或只清 dangling悬挂的、无引用的中间层
docker builder prune -f --filter "type=regular"
# ✅ 清旧镜像(不影响构建缓存)
docker image prune -a # 删所有未被容器使用的镜像
docker image prune # 只删 dangling 镜像
# ✅ 清 pip wheel 缓存BuildKit cache mount安全
docker builder prune -f --filter "type=exec.cachemount"
# ❌ 永远不要这样用——会清掉仍在用的稳定 base 层
docker builder prune -f --filter "until=30m"
docker builder prune -f --filter "until=24h"
```
> 根因BuildKit 的 `until` filter 按"最后访问时间"判定,而非"是否仍在被引用"。CACHED
> 跳过的层不会刷新访问时间,于是被误判为可回收。这是 BuildKit 的已知行为,不是 bug
> 但对"稳定 base + 频繁改代码"的构建模式特别致命。详见
> [moby/buildkit#2414](https://github.com/moby/buildkit/issues/2414)。
#### 什么时候需要主动清缓存
- **磁盘紧张**`docker builder prune -f` + `docker image prune` 释放空间
- **依赖换了 torch / CUDA 大版本**BuildKit 可能复用了不兼容的旧 wheel清 pip 缓存
mount 强制重下:`docker builder prune -f --filter "type=exec.cachemount"`
- **换 VARIANTcpu↔gpu**:不需要清——两条构建链独立,缓存互不干扰
- **想从零验证构建**`docker builder prune -af` 清全部,模拟新机器首次构建
#### 模型缓存(`./models`
模型权重在 `./models` volume容器内 `HF_HOME=/models/huggingface`
`CT2_CACHE=/models/ctranslate2`),跨容器复用。首次启动下载,之后秒起。
```bash
# 查看模型缓存大小
du -sh ./models
# 删了强制重下GPU 大模型 ~5.5GB,建议用 prefetch 脚本提前下好)
rm -rf ./models
./scripts/prefetch_models.sh config.gpu.yaml
```
### Volume 挂载
| 容器路径 | 宿主路径 | 用途 | 删除影响 |
|---|---|---|---|
| `/data` | `./data`CPU/ `./data-gpu`GPU | 上传视频、中间音频、输出字幕、SQLite | 任务历史和产物全没 |
| `/models` | `./models` | 模型缓存HF + ctranslate2跨容器复用 | 下次启动重下模型 |
| `/app/config.yaml` | `./config.yaml`(只读) | 配置文件 | 改配置需重启容器 |
镜像本身无状态、无敏感数据。
### ctranslate2 可执行栈修复 ### ctranslate2 可执行栈修复
ctranslate2 的 `.so`(在 `ctranslate2.libs/` 隐藏目录)带 PT_GNU_STACK 可执行栈标志, ctranslate2 的 `.so`(在 `ctranslate2.libs/` 隐藏目录)带 PT_GNU_STACK 可执行栈标志,