mirror of
https://github.com/modelscope/FunASR.git
synced 2026-10-12 02:19:18 +00:00
Merge pull request #3660 from modelscope/codex/vllm-single-source-20260907
docs: consolidate Chinese vLLM guides with checked compatibility mirror
This commit is contained in:
@@ -14,6 +14,7 @@ on:
|
||||
- 'runtime/**/*.md'
|
||||
- 'examples/openai_api/**/*.md'
|
||||
- 'scripts/gen_api_docs.py'
|
||||
- 'scripts/sync_vllm_guide.py'
|
||||
- 'tests/test_*docs*.py'
|
||||
- 'tests/test_api_signatures.py'
|
||||
- 'gh-pages-output/**'
|
||||
@@ -35,6 +36,7 @@ on:
|
||||
- 'runtime/**/*.md'
|
||||
- 'examples/openai_api/**/*.md'
|
||||
- 'scripts/gen_api_docs.py'
|
||||
- 'scripts/sync_vllm_guide.py'
|
||||
- 'tests/test_*docs*.py'
|
||||
- 'tests/test_api_signatures.py'
|
||||
- 'gh-pages-output/**'
|
||||
@@ -81,12 +83,16 @@ jobs:
|
||||
run: python -m pip install -r web-pages/product-site/requirements-site.txt
|
||||
- name: Run product-site tests
|
||||
run: python -m pytest web-pages/product-site/tests -q
|
||||
- name: Check generated vLLM compatibility guide
|
||||
run: python scripts/sync_vllm_guide.py --check
|
||||
- name: Run model-free documentation contracts
|
||||
run: >-
|
||||
python -m pytest
|
||||
tests/test_learning_docs_contract.py
|
||||
tests/test_reference_docs_contract.py
|
||||
tests/test_vllm_examples_contract.py
|
||||
tests/test_vllm_guide_sync_docs.py
|
||||
tests/test_vllm_model_source_docs.py
|
||||
tests/test_training_docs_contract.py
|
||||
tests/test_python_api_docs_contract.py
|
||||
tests/test_streaming_vad_docs.py
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
paths:
|
||||
- 'funasr/**/*.py'
|
||||
- 'scripts/gen_api_docs.py'
|
||||
- 'scripts/sync_vllm_guide.py'
|
||||
- 'training.html'
|
||||
- 'docs/**/*.md'
|
||||
- 'examples/openai_api/**/*.md'
|
||||
@@ -30,6 +31,9 @@ jobs:
|
||||
- name: Generate API docs
|
||||
run: python3 scripts/gen_api_docs.py
|
||||
|
||||
- name: Check generated vLLM compatibility guide
|
||||
run: python3 scripts/sync_vllm_guide.py --check
|
||||
|
||||
- name: Render shared documentation sources
|
||||
run: |
|
||||
python3 -m pip install -r web-pages/product-site/requirements-site.txt
|
||||
|
||||
+28
-18
@@ -34,23 +34,27 @@
|
||||
|
||||
## 1. 安装与环境
|
||||
|
||||
先安装 vLLM,按 NVIDIA 驱动的 CUDA 版本选对应版本;vLLM 会自动钉定并安装匹配的 torch / torchaudio / torchvision 三件套,所以不要自己装 torch/torchaudio——三者 ABI 锁死,必须是互相编译匹配的同一组(如 torch 2.10.0 ↔ torchaudio 2.10.0 ↔ torchvision 0.25.0),只能随 vLLM 一起来。
|
||||
本指南的 SDK、离线与 WebSocket 服务使用 **FunASR 拆分引擎**;不要把它的环境与下文原生 `vllm serve` 的验证环境混用。先选定 vLLM 版本和对应的 GPU wheel,再在独立虚拟环境中安装。`nvidia-smi` 的 CUDA 数字是驱动支持的上限,不是已安装的 CUDA runtime,也不能仅凭“12.x / 13.x”判断 wheel 是否兼容。
|
||||
|
||||
下面以拆分引擎使用过的 `vllm==0.19.1` 和固定 FunASR 源码为起点。它固定了两个项目的版本,但**不是完整依赖锁文件,也不是所有 GPU 的干净安装验收**。GPU 构建与驱动要求请核对该版本的 [vLLM 安装文档](https://github.com/vllm-project/vllm/blob/v0.19.1/docs/getting_started/installation/gpu.md)。
|
||||
|
||||
```bash
|
||||
# 1) 先装 vLLM。按 `nvidia-smi` 显示的 CUDA 版本(驱动支持的最高 CUDA,不是 runtime CUDA)选版本,
|
||||
# vLLM 会带来匹配的 torch/torchaudio/torchvision。
|
||||
# 驱动 CUDA 12.x -> pip install vllm==0.19.1 (附带 torch 2.10 / cu128)
|
||||
# 驱动 CUDA >= 13 -> pip install vllm (最新版;附带 torch 2.11 / cu130)
|
||||
python3.12 -m venv .venv-funasr-vllm
|
||||
source .venv-funasr-vllm/bin/activate
|
||||
python -m pip install "vllm==0.19.1"
|
||||
|
||||
pip install "vllm==0.19.1" # 按你的驱动 CUDA 调整;见下方说明
|
||||
|
||||
# 2) 再装 FunASR 与其余依赖。
|
||||
pip install "funasr>=1.3.26"
|
||||
pip install safetensors tiktoken websockets regex fastapi uvicorn python-multipart
|
||||
|
||||
cd /path/to/FunASR && pip install -e .
|
||||
# 服务脚本来自源码;在新的目录中安装,不覆盖已有工作区。
|
||||
git clone https://github.com/modelscope/FunASR.git FunASR-vllm
|
||||
cd FunASR-vllm
|
||||
git checkout --detach e42443f55971d0c804dcf2973fdd2e6e09bd5611
|
||||
python -m pip install -e .
|
||||
python -m pip install safetensors tiktoken websockets regex fastapi uvicorn python-multipart
|
||||
python -m pip check
|
||||
python -m pip freeze > environment.txt
|
||||
```
|
||||
|
||||
保存 GPU/驱动、Python、源码提交、模型 revision 和 `environment.txt`,再执行单请求、真实 WebSocket 及目标并发验收。`pip check` 只能检查声明的依赖关系,不证明 CUDA、音频算子或端到端服务可用。单纯安装 PyPI 包不会提供本指南引用的仓库服务脚本。
|
||||
|
||||
### 开始前先选定模型路径
|
||||
|
||||
目前有两条不同的 Fun-ASR-Nano vLLM 集成路径,checkpoint 与调用接口不能混用:
|
||||
@@ -86,14 +90,16 @@ model = AutoModelVLLM(
|
||||
|
||||
#### B. vLLM 原生转写路径
|
||||
|
||||
官方维护的 native checkpoint 是
|
||||
原生 `FunASRForConditionalGeneration` 使用完整 native checkpoint,不是路径 A 的 `model.pt` 拆分布局。官方维护的 native checkpoint 是
|
||||
[`FunAudioLLM/Fun-ASR-Nano-2512-vllm`](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512-vllm)。
|
||||
vLLM 的支持模型表也把
|
||||
[`allendou/Fun-ASR-Nano-2512-vllm`](https://huggingface.co/allendou/Fun-ASR-Nano-2512-vllm)
|
||||
列为原生 `FunASRForConditionalGeneration` 架构示例;后者是托管在官方
|
||||
FunAudioLLM 组织之外的社区转换完整 checkpoint。只有明确选择 vLLM 原生转写
|
||||
接口时,才应使用这两种 native checkpoint;不要用它们替换下文 FunASR
|
||||
`AutoModelVLLM` 示例中的官方 checkpoint。
|
||||
`AutoModelVLLM` 示例中的官方 checkpoint,也不要传给
|
||||
[serve_realtime_ws.py](../examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py);这些服务预期
|
||||
`model.pt`、`config.yaml` 与 `Qwen3-0.6B/`,不能用 native layout 替代。
|
||||
|
||||
两种 native 路径都通过 `vllm serve` 提供非实时的请求/响应式转写
|
||||
`/v1/audio/transcriptions`,不会注册 `/v1/realtime`。vLLM 只为声明
|
||||
@@ -102,9 +108,13 @@ realtime 任务的模型注册该 WebSocket 端点,`FunASRForConditionalGenera
|
||||
中。需要实时流式识别时,请使用 FunASR 流式 SDK 推理或流式 ASR 服务;路径 A 的
|
||||
`AutoModelVLLM` 示例同样属于离线推理。
|
||||
|
||||
**硬件**:GPU ≥ 8GB VRAM,CUDA ≥ 11.8。推荐 16GB+。
|
||||
原生 HTTP API 不会自动获得 FunASR WebSocket 服务的 VAD、partial 预览、会话状态或 SPK 处理。WebSocket 握手的拒绝状态不是稳定的 API 契约,也不是模型支持实时转写的探测方法。
|
||||
|
||||
为什么不要单独执行 `pip install torch torchaudio` ? torch/torchaudio/torchvision 的版本由 vLLM 版本决定—— 每个大版本会一起升级(见 vLLM 的 [requirements/cuda.txt](https://github.com/vllm-project/vllm/blob/main/requirements/cuda.txt))。手动安装会拉到最新 wheel,可能是为比你驱动更新的 CUDA runtime 编译的;PyTorch 会在 CUDA 初始化阶段、FunASR 启动前就报 The NVIDIA driver on your system is too old。让 vLLM 统一钉定这三件套即可避免。若仍遇到该错误,请安装其 CUDA 构建与 nvidia-smi 显示的 CUDA 匹配的 vLLM 版本(如 CUDA 12.x 用 vllm==0.19.1),或先升级 NVIDIA 驱动。
|
||||
官方权重的固定 revision、启动参数和实际输出见 [原生转写验证记录](./vllm_official_native_validation_zh.md)。该记录仅覆盖既有 H100 环境中的 vLLM 0.27.1 文件转写,不是干净安装、精度、容量、长音频、实时流式或说话人分离验收;不要把它的环境替换到上面的 0.19.1 拆分引擎命令中。
|
||||
|
||||
**硬件**:以所选 vLLM GPU 构建的要求为准。显存需求还取决于模型、精度、KV cache、batch 和会话数;本指南不承诺统一的最低显存或并发容量。
|
||||
|
||||
不要在已验证环境中单独升级 `torch` 或 `torchaudio`。依赖约束随 vLLM 版本变化,不能概括成“始终自动安装相同版本的三件套”:例如 [0.19.1 的发布元数据](https://pypi.org/pypi/vllm/0.19.1/json)声明 `torch==2.10.0`、`torchaudio==2.10.0`、`torchvision==0.25.0`;[0.27.1 的发布元数据](https://pypi.org/pypi/vllm/0.27.1/json)声明 `torch==2.13.0`、`torchaudio==2.11.0`、`torchvision==0.28.0`。这些只是声明约束,不是安装成功或 ABI 兼容的证明。升级时新建环境并重新验收;遇到驱动过旧错误,应检查实际 wheel 的 CUDA 构建与驱动要求,而不是盲目安装最新版。
|
||||
|
||||
---
|
||||
|
||||
@@ -821,7 +831,7 @@ Fun-ASR-Nano 的声学编码器(SenseVoice)是**全上下文、非流式**
|
||||
- **先把单进程作为第一扩展单元。** 先用内置批处理压测单进程;只有当单进程达到实测 GPU、CPU 或尾延迟上限后,再增加进程或按 GPU 横向扩容。每个额外进程都会复制模型显存,也可能减少单进程内形成 batch 的机会。
|
||||
- **vLLM 收益取决于请求是否同时到达。** 真实轮流对话可能连接很多,但同时解码很少;把同一段连续独白同步回放给所有客户端,则会刻意形成大批次。发布结果时必须同时报告话务形态和批处理参数。
|
||||
- **可持续并发没有通用的“支持 N 路”数字。** 上限主要取决于同时说话数、静音比例、句长、partial 刷新间隔、说话人分离、batch 等待时间以及 GPU/CPU 能力。长时间不停顿的语音仍会重复编码临时窗口(见 §6.5),成本更高。请按自己的真实话务压测,不要把其他部署的连接数直接当成规格。
|
||||
- **L20 实测起点,不是全局默认值。** [#3528](https://github.com/modelscope/FunASR/issues/3528) 在单张 L20 上同步回放 47 秒连续语音、16 路客户端,并关闭 SPK 与客户端 ping;`--partial-window-sec 8 --decode-interval 2.0` 是该负载的最优组合:408 次解码请求、累计编码 3,072.1 秒音频、完成 p50 51.18 秒、输出滞后 4.5 秒、聚合实时率 14.2x、首词 1.31 秒。默认 15 秒窗口在这组 16 路负载上未完成。请只把 `8 / 2.0` 当作 L20 高并发初始配置,再按自己的首词、输出滞后、最终完成时间、请求数和累计编码量调优。
|
||||
- **L20 历史实测起点,不是容量承诺。** [#3528](https://github.com/modelscope/FunASR/issues/3528) 的一组历史测量在单张 L20 上同步回放 47 秒连续语音、16 路客户端,并关闭 SPK 与客户端 ping;`--partial-window-sec 8 --decode-interval 2.0` 是该组测量中表现最好的配置:408 次解码请求、累计编码 3,072.1 秒音频、完成 p50 51.18 秒、输出滞后 4.5 秒、聚合实时率 14.2x、首词 1.31 秒。该组测量当时采用的默认 15 秒窗口在 16 路负载上未完成;这不是当前源码的默认值(当前为 8 秒),也不能代替后续版本和不同 keepalive 配置的结果。请只把 `8 / 2.0` 当作该话务的调优起点,记录版本、窗口、keepalive、首词、输出滞后、最终完成时间、请求数和累计编码量。Git 安装成功不等于并发问题解决;该问题仍需独立验收。
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 python examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py \
|
||||
@@ -914,7 +924,7 @@ print(segments[0]["value"])
|
||||
路径或解码参数和 upstream runner 不一致。改模型前先检查这些项:
|
||||
|
||||
- 传给 vLLM 的 prompt embeddings 要显式转成 float32:
|
||||
`EmbedsPrompt(prompt_embeds=input_embeds.float())`。
|
||||
`EmbedsPrompt` 的 `prompt_embeds` 参数应接收 `input_embeds.float()`。
|
||||
- 使用 ASR 更合适的确定性解码。Fun-ASR-Nano vLLM 路径默认使用
|
||||
`temperature=0.0`、`top_p=1.0` 和 `skip_special_tokens=True`。在
|
||||
prompt-embeds 模式下,`repetition_penalty` 保持中性的 `1.0`,除非你走的是
|
||||
|
||||
+107
-36
@@ -1,3 +1,6 @@
|
||||
<!-- Generated by scripts/sync_vllm_guide.py; edit vllm_guide_zh.md. -->
|
||||
> This compatibility page is generated from the [maintained Chinese guide](./vllm_guide_zh.md). Edit that source and run `python scripts/sync_vllm_guide.py`.
|
||||
|
||||
# FunASR vLLM 推理引擎指南
|
||||
|
||||
---
|
||||
@@ -34,23 +37,27 @@
|
||||
|
||||
## 1. 安装与环境
|
||||
|
||||
先安装 vLLM,按 NVIDIA 驱动的 CUDA 版本选对应版本;vLLM 会自动钉定并安装匹配的 torch / torchaudio / torchvision 三件套,所以不要自己装 torch/torchaudio——三者 ABI 锁死,必须是互相编译匹配的同一组(如 torch 2.10.0 ↔ torchaudio 2.10.0 ↔ torchvision 0.25.0),只能随 vLLM 一起来。
|
||||
本指南的 SDK、离线与 WebSocket 服务使用 **FunASR 拆分引擎**;不要把它的环境与下文原生 `vllm serve` 的验证环境混用。先选定 vLLM 版本和对应的 GPU wheel,再在独立虚拟环境中安装。`nvidia-smi` 的 CUDA 数字是驱动支持的上限,不是已安装的 CUDA runtime,也不能仅凭“12.x / 13.x”判断 wheel 是否兼容。
|
||||
|
||||
下面以拆分引擎使用过的 `vllm==0.19.1` 和固定 FunASR 源码为起点。它固定了两个项目的版本,但**不是完整依赖锁文件,也不是所有 GPU 的干净安装验收**。GPU 构建与驱动要求请核对该版本的 [vLLM 安装文档](https://github.com/vllm-project/vllm/blob/v0.19.1/docs/getting_started/installation/gpu.md)。
|
||||
|
||||
```bash
|
||||
# 1) 先装 vLLM。按 `nvidia-smi` 显示的 CUDA 版本(驱动支持的最高 CUDA,不是 runtime CUDA)选版本,
|
||||
# vLLM 会带来匹配的 torch/torchaudio/torchvision。
|
||||
# 驱动 CUDA 12.x -> pip install vllm==0.19.1 (附带 torch 2.10 / cu128)
|
||||
# 驱动 CUDA >= 13 -> pip install vllm (最新版;附带 torch 2.11 / cu130)
|
||||
python3.12 -m venv .venv-funasr-vllm
|
||||
source .venv-funasr-vllm/bin/activate
|
||||
python -m pip install "vllm==0.19.1"
|
||||
|
||||
pip install "vllm==0.19.1" # 按你的驱动 CUDA 调整;见下方说明
|
||||
|
||||
# 2) 再装 FunASR 与其余依赖。
|
||||
pip install "funasr>=1.3.26"
|
||||
pip install safetensors tiktoken websockets regex fastapi uvicorn python-multipart
|
||||
|
||||
cd /path/to/FunASR && pip install -e .
|
||||
# 服务脚本来自源码;在新的目录中安装,不覆盖已有工作区。
|
||||
git clone https://github.com/modelscope/FunASR.git FunASR-vllm
|
||||
cd FunASR-vllm
|
||||
git checkout --detach e42443f55971d0c804dcf2973fdd2e6e09bd5611
|
||||
python -m pip install -e .
|
||||
python -m pip install safetensors tiktoken websockets regex fastapi uvicorn python-multipart
|
||||
python -m pip check
|
||||
python -m pip freeze > environment.txt
|
||||
```
|
||||
|
||||
保存 GPU/驱动、Python、源码提交、模型 revision 和 `environment.txt`,再执行单请求、真实 WebSocket 及目标并发验收。`pip check` 只能检查声明的依赖关系,不证明 CUDA、音频算子或端到端服务可用。单纯安装 PyPI 包不会提供本指南引用的仓库服务脚本。
|
||||
|
||||
### 开始前先选定模型路径
|
||||
|
||||
目前有两条不同的 Fun-ASR-Nano vLLM 集成路径,checkpoint 与调用接口不能混用:
|
||||
@@ -86,33 +93,31 @@ model = AutoModelVLLM(
|
||||
|
||||
#### B. vLLM 原生转写路径
|
||||
|
||||
vLLM 原生 `FunASRForConditionalGeneration` 路径使用完整的 native checkpoint,
|
||||
不是上文的 FunASR `model.pt` 布局。官方维护的模型为
|
||||
[`FunAudioLLM/Fun-ASR-Nano-2512-vllm`](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512-vllm):
|
||||
|
||||
```bash
|
||||
vllm serve FunAudioLLM/Fun-ASR-Nano-2512-vllm --port 8000
|
||||
```
|
||||
|
||||
vLLM 的支持模型表也曾使用
|
||||
原生 `FunASRForConditionalGeneration` 使用完整 native checkpoint,不是路径 A 的 `model.pt` 拆分布局。官方维护的 native checkpoint 是
|
||||
[`FunAudioLLM/Fun-ASR-Nano-2512-vllm`](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512-vllm)。
|
||||
vLLM 的支持模型表也把
|
||||
[`allendou/Fun-ASR-Nano-2512-vllm`](https://huggingface.co/allendou/Fun-ASR-Nano-2512-vllm)
|
||||
这一社区转换示例。它同样必须通过 `vllm serve` 的原生接口使用;不要把任一
|
||||
native checkpoint 传给 `AutoModelVLLM` 或
|
||||
`examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py`。后两者预期
|
||||
官方 `model.pt`、`config.yaml` 与 `Qwen3-0.6B/` 目录,因而会拒绝 native layout。
|
||||
列为原生 `FunASRForConditionalGeneration` 架构示例;后者是托管在官方
|
||||
FunAudioLLM 组织之外的社区转换完整 checkpoint。只有明确选择 vLLM 原生转写
|
||||
接口时,才应使用这两种 native checkpoint;不要用它们替换下文 FunASR
|
||||
`AutoModelVLLM` 示例中的官方 checkpoint,也不要传给
|
||||
[serve_realtime_ws.py](../examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py);这些服务预期
|
||||
`model.pt`、`config.yaml` 与 `Qwen3-0.6B/`,不能用 native layout 替代。
|
||||
|
||||
原生 `vllm serve` 的转写 API 不会自动获得 FunASR WebSocket 服务的 VAD、partial
|
||||
预览、会话状态或说话人处理,因此 native Fun-ASR 模型没有可用的实时转写会话。
|
||||
即使对 `/v1/realtime` 发起 WebSocket 握手,也不能据此判断模型支持实时转写;当前
|
||||
vLLM 服务会拒绝这类握手,Uvicorn 通常显示 `403 Forbidden`。这是当前实现的拒绝表现,
|
||||
不是稳定的 API 契约,也不应据此编写支持性探测。需要实时行为时,请按 vLLM 的
|
||||
[realtime speech-to-text 示例](https://docs.vllm.ai/en/latest/examples/speech_to_text/realtime/)
|
||||
在应用侧实现音频分段、请求调度与可替换预览;需要 FunASR 已实现的 WebSocket
|
||||
协议时,继续使用上文的官方 checkpoint + `serve_realtime_ws.py` 路径。
|
||||
两种 native 路径都通过 `vllm serve` 提供非实时的请求/响应式转写
|
||||
`/v1/audio/transcriptions`,不会注册 `/v1/realtime`。vLLM 只为声明
|
||||
realtime 任务的模型注册该 WebSocket 端点,`FunASRForConditionalGeneration`
|
||||
当前不在它的 [Realtime Transcription 表](https://docs.vllm.ai/en/latest/models/supported_models/#realtime-transcription)
|
||||
中。需要实时流式识别时,请使用 FunASR 流式 SDK 推理或流式 ASR 服务;路径 A 的
|
||||
`AutoModelVLLM` 示例同样属于离线推理。
|
||||
|
||||
**硬件**:GPU ≥ 8GB VRAM,CUDA ≥ 11.8。推荐 16GB+。
|
||||
原生 HTTP API 不会自动获得 FunASR WebSocket 服务的 VAD、partial 预览、会话状态或 SPK 处理。WebSocket 握手的拒绝状态不是稳定的 API 契约,也不是模型支持实时转写的探测方法。
|
||||
|
||||
为什么不要单独执行 `pip install torch torchaudio` ? torch/torchaudio/torchvision 的版本由 vLLM 版本决定—— 每个大版本会一起升级(见 vLLM 的 [requirements/cuda.txt](https://github.com/vllm-project/vllm/blob/main/requirements/cuda.txt))。手动安装会拉到最新 wheel,可能是为比你驱动更新的 CUDA runtime 编译的;PyTorch 会在 CUDA 初始化阶段、FunASR 启动前就报 The NVIDIA driver on your system is too old。让 vLLM 统一钉定这三件套即可避免。若仍遇到该错误,请安装其 CUDA 构建与 nvidia-smi 显示的 CUDA 匹配的 vLLM 版本(如 CUDA 12.x 用 vllm==0.19.1),或先升级 NVIDIA 驱动。
|
||||
官方权重的固定 revision、启动参数和实际输出见 [原生转写验证记录](./vllm_official_native_validation_zh.md)。该记录仅覆盖既有 H100 环境中的 vLLM 0.27.1 文件转写,不是干净安装、精度、容量、长音频、实时流式或说话人分离验收;不要把它的环境替换到上面的 0.19.1 拆分引擎命令中。
|
||||
|
||||
**硬件**:以所选 vLLM GPU 构建的要求为准。显存需求还取决于模型、精度、KV cache、batch 和会话数;本指南不承诺统一的最低显存或并发容量。
|
||||
|
||||
不要在已验证环境中单独升级 `torch` 或 `torchaudio`。依赖约束随 vLLM 版本变化,不能概括成“始终自动安装相同版本的三件套”:例如 [0.19.1 的发布元数据](https://pypi.org/pypi/vllm/0.19.1/json)声明 `torch==2.10.0`、`torchaudio==2.10.0`、`torchvision==0.25.0`;[0.27.1 的发布元数据](https://pypi.org/pypi/vllm/0.27.1/json)声明 `torch==2.13.0`、`torchaudio==2.11.0`、`torchvision==0.28.0`。这些只是声明约束,不是安装成功或 ABI 兼容的证明。升级时新建环境并重新验收;遇到驱动过旧错误,应检查实际 wheel 的 CUDA 构建与驱动要求,而不是盲目安装最新版。
|
||||
|
||||
---
|
||||
|
||||
@@ -668,6 +673,33 @@ CUDA_VISIBLE_DEVICES=0 python examples/industrial_data_pretraining/fun_asr_nano/
|
||||
|
||||
说话人分离默认关闭;只有确实需要 `spk` 字段时再加 `--enable-spk`。
|
||||
|
||||
服务端默认每 20 秒发送一次 WebSocket ping,但不因 ping 超时主动断开连接。
|
||||
在长音频高并发下,模型推理和 VAD 处理会延迟控制帧,即使连接仍然健康;
|
||||
固定 timeout 因此可能在计算或排队反压期间误杀正常会话。
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 python examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py \
|
||||
--port 10095 --language 中文
|
||||
```
|
||||
|
||||
只有在按生产流量测得最坏推理和排队延迟后,才设置正数
|
||||
`--ws-ping-timeout`;该值应高于实测延迟,并与网关 idle timeout 策略配合。
|
||||
`websockets` 库的 `max_queue` 设置只限制入站消息的接收缓冲,不会改变
|
||||
ping/pong 的超时语义,因此增大它不能解决 keepalive timeout。只有外部网关
|
||||
已经统一负责 keepalive / reconnect 策略时,才设置 `--ws-ping-interval 0`
|
||||
关闭服务端 ping。
|
||||
|
||||
长会话排障,尤其是启用 `--enable-spk` 时,可以打开周期性 session 状态日志:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 python examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py \
|
||||
--port 10095 --language 中文 --enable-spk \
|
||||
--log-session-stats-interval 30
|
||||
```
|
||||
|
||||
服务端会每 30 秒输出一行 `Session stats:`。提交 issue 时请连同最后几行
|
||||
`Session stats`、末段 RTF、进程 RSS、GPU memory 和断线前后的服务端日志一起提供。
|
||||
|
||||
### 6.3 WebSocket 协议
|
||||
|
||||
**连接**:`ws://host:10095`
|
||||
@@ -717,6 +749,15 @@ python client_python.py --server ws://localhost:10095 --mic
|
||||
python client_python.py --server ws://localhost:10095 --file audio.wav
|
||||
```
|
||||
|
||||
**实时压测**:
|
||||
```bash
|
||||
python examples/industrial_data_pretraining/fun_asr_nano/realtime_ws_benchmark.py \
|
||||
audio_16k_mono_pcm16.wav --server ws://localhost:10095 --clients 4 \
|
||||
--output-jsonl realtime_ws_4c.jsonl
|
||||
```
|
||||
|
||||
指标定义和报告字段见 [Realtime WebSocket Benchmark](./benchmark/realtime_ws_benchmark.md)。
|
||||
|
||||
**浏览器**:打开 `client_mic.html`
|
||||
|
||||
**自定义 Python**:
|
||||
@@ -773,7 +814,7 @@ Fun-ASR-Nano 的声学编码器(SenseVoice)是**全上下文、非流式**
|
||||
|
||||
**使用建议**
|
||||
- 正常对话语音有自然停顿,VAD 会把它切成一句句较短的语音,每句 partial 的开销自然受限,**通常无需关注**。
|
||||
- 只有**超长、不停顿的连续语音**(如长篇朗读)会让单句不断变长、partial 预览逐渐变慢。`serve_realtime_ws.py` 默认用 `--partial-window-sec 8` 限制临时预览窗口;多客户端或连续独白压测应先从该值开始,并只在实测有余量时再提高窗口。它只影响临时 `partial`,VAD 锁定句和 STOP 最终结果仍走完整音频。
|
||||
- 只有**超长、不停顿的连续语音**(如长篇朗读)会让单句不断变长、partial 预览逐渐变慢。`serve_realtime_ws.py` 默认用 `--partial-window-sec 8` 限制临时预览窗口;多客户端连续独白负载应先从该值开始,并只在实测有余量时再提高窗口。它只影响临时 `partial`,VAD 锁定句和 STOP 最终结果仍走完整音频。可先参考 §6.7 的 L20 实测起点。
|
||||
|
||||
### 6.6 说话人分离(SPK)的代价与开关
|
||||
|
||||
@@ -782,6 +823,7 @@ Fun-ASR-Nano 的声学编码器(SenseVoice)是**全上下文、非流式**
|
||||
- **Fun-ASR-Nano 上 SPK 效果有限**(见 #2944),多数实时 ASR 场景并不需要说话人分离。
|
||||
- **流式 SPK 代价高且随会话变长**:每句会在该 session 的 worker 中对**全部历史 embedding** 做一次全量重聚类(**O(N²)**,会话越长每句越贵);而会话结束时还会**全量重聚一遍**,流式期间每句的聚类结果会被最终结果覆盖——对最终输出而言属于重复计算。长会话 + 高并发下尤其明显。
|
||||
- **建议**:多客户端实时转写优先保持默认关闭;确需 diarization 时再加 `--enable-spk`,并以 STOP 后的最终 `spk` 标签为准。
|
||||
- **长会话诊断**:如果 session 仍然逐渐变慢或断开,请用 `--log-session-stats-interval 30` 复测,并观察 `audio_buffer_samples`、`locked_sentences`、`speaker_history_chunks`、`speaker_history_embeddings` 和 `speaker_centers` 是否保持有界。如果这些计数都接近上限但 RTF 仍持续升高,剩余瓶颈更可能在模型推理、返回 payload 大小或环境调度,而不是 session 状态继续泄漏。
|
||||
|
||||
### 6.7 生产并发与多进程部署
|
||||
|
||||
@@ -792,6 +834,12 @@ Fun-ASR-Nano 的声学编码器(SenseVoice)是**全上下文、非流式**
|
||||
- **先把单进程作为第一扩展单元。** 先用内置批处理压测单进程;只有当单进程达到实测 GPU、CPU 或尾延迟上限后,再增加进程或按 GPU 横向扩容。每个额外进程都会复制模型显存,也可能减少单进程内形成 batch 的机会。
|
||||
- **vLLM 收益取决于请求是否同时到达。** 真实轮流对话可能连接很多,但同时解码很少;把同一段连续独白同步回放给所有客户端,则会刻意形成大批次。发布结果时必须同时报告话务形态和批处理参数。
|
||||
- **可持续并发没有通用的“支持 N 路”数字。** 上限主要取决于同时说话数、静音比例、句长、partial 刷新间隔、说话人分离、batch 等待时间以及 GPU/CPU 能力。长时间不停顿的语音仍会重复编码临时窗口(见 §6.5),成本更高。请按自己的真实话务压测,不要把其他部署的连接数直接当成规格。
|
||||
- **L20 历史实测起点,不是容量承诺。** [#3528](https://github.com/modelscope/FunASR/issues/3528) 的一组历史测量在单张 L20 上同步回放 47 秒连续语音、16 路客户端,并关闭 SPK 与客户端 ping;`--partial-window-sec 8 --decode-interval 2.0` 是该组测量中表现最好的配置:408 次解码请求、累计编码 3,072.1 秒音频、完成 p50 51.18 秒、输出滞后 4.5 秒、聚合实时率 14.2x、首词 1.31 秒。该组测量当时采用的默认 15 秒窗口在 16 路负载上未完成;这不是当前源码的默认值(当前为 8 秒),也不能代替后续版本和不同 keepalive 配置的结果。请只把 `8 / 2.0` 当作该话务的调优起点,记录版本、窗口、keepalive、首词、输出滞后、最终完成时间、请求数和累计编码量。Git 安装成功不等于并发问题解决;该问题仍需独立验收。
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 python examples/industrial_data_pretraining/fun_asr_nano/serve_realtime_ws.py \
|
||||
--partial-window-sec 8 --decode-interval 2.0 --log-decode-profile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -867,3 +915,26 @@ print(segments[0]["value"])
|
||||
|
||||
**Q: 首次慢?**
|
||||
模型加载、KV cache 分配及可选 CUDA Graph 预热都会影响首次启动。请分别测量冷启动与预热后推理,二者均没有固定耗时或即时返回保证。
|
||||
|
||||
**Q: Fun-ASR-Nano vLLM 使用 `dtype="fp16"` 时实际会怎样?**
|
||||
音频 frontend 与 adaptor 仍使用 FP16,但 FunASR 会让 Qwen3 decoder 使用 BF16,
|
||||
因为 vLLM 的 FP16 decoder 可能产生退化的重复输出。该行为自动生效,decoder 权重
|
||||
仍是两字节精度;不支持 BF16 的硬件请使用 `dtype="fp32"`。vLLM 路径不宣称支持
|
||||
端到端 FP16 decoder。
|
||||
|
||||
**Q: vLLM 输出连续标点(例如 `!!!!!!!!`),但 PyTorch/HF generate 正常,应该先查什么?**
|
||||
这通常说明音频 frontend 和 checkpoint 本身能工作,但 vLLM prompt-embedding
|
||||
路径或解码参数和 upstream runner 不一致。改模型前先检查这些项:
|
||||
|
||||
- 传给 vLLM 的 prompt embeddings 要显式转成 float32:
|
||||
`EmbedsPrompt` 的 `prompt_embeds` 参数应接收 `input_embeds.float()`。
|
||||
- 使用 ASR 更合适的确定性解码。Fun-ASR-Nano vLLM 路径默认使用
|
||||
`temperature=0.0`、`top_p=1.0` 和 `skip_special_tokens=True`。在
|
||||
prompt-embeds 模式下,`repetition_penalty` 保持中性的 `1.0`,除非你走的是
|
||||
token prompt 路径;FunASR 的 vLLM helper 会把其他值归一化,避免 vLLM CUDA scatter
|
||||
错误。
|
||||
- 确认 `model_dir` 和 `vllm_model_dir` 是匹配的一组 Fun-ASR-Nano 模型。如果清空
|
||||
`vllm_model_dir` 后同一音频走 HF generate 正常,就继续排查 vLLM 路径,而不是音频文件。
|
||||
- 对一个失败样本记录 vLLM `finish_reason`、生成 token ids、prompt embedding dtype
|
||||
和 shape。连续标点且 `finish_reason="length"` 时,通常更像解码/prompt 不匹配,而不是
|
||||
VAD 或音频读取问题。
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Keep the old Chinese vLLM guide URL as a full, generated Markdown mirror."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "docs/vllm_guide_zh.md"
|
||||
TARGET = ROOT / "docs/vllm_guide_zh_v2.md"
|
||||
NOTICE = (
|
||||
"<!-- Generated by scripts/sync_vllm_guide.py; edit vllm_guide_zh.md. -->\n"
|
||||
"> This compatibility page is generated from the "
|
||||
"[maintained Chinese guide](./vllm_guide_zh.md). "
|
||||
"Edit that source and run `python scripts/sync_vllm_guide.py`.\n\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true", help="Fail on drift without writing")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
expected = NOTICE + SOURCE.read_bytes()
|
||||
if args.check:
|
||||
if not TARGET.is_file() or TARGET.read_bytes() != expected:
|
||||
print("Run python scripts/sync_vllm_guide.py to update the mirror.", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
TARGET.write_bytes(expected)
|
||||
except OSError as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -7,8 +7,6 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
DOCS_WITH_CURRENT_FUNASR_INSTALL = [
|
||||
"docs/vllm_guide.md",
|
||||
"docs/vllm_guide_zh.md",
|
||||
"docs/vllm_guide_zh_v2.md",
|
||||
"examples/industrial_data_pretraining/fun_asr_nano/docs/finetune.md",
|
||||
"examples/industrial_data_pretraining/fun_asr_nano/docs/finetune_zh.md",
|
||||
]
|
||||
@@ -70,6 +68,18 @@ def test_current_funasr_install_commands_are_quoted():
|
||||
assert not re.search(r"pip install funasr>=", text)
|
||||
|
||||
|
||||
def test_chinese_vllm_service_install_uses_a_pinned_checkout():
|
||||
text = (ROOT / "docs/vllm_guide_zh.md").read_text()
|
||||
blocks = re.findall(r"^```bash\n(.*?)^```", text, re.M | re.S)
|
||||
install = blocks[0]
|
||||
assert 'python -m pip install "vllm==0.19.1"' in install
|
||||
assert "git clone https://github.com/modelscope/FunASR.git FunASR-vllm" in install
|
||||
assert re.search(r"^git checkout --detach [0-9a-f]{40}$", install, re.M)
|
||||
assert "python -m pip install -e ." in install
|
||||
assert "python -m pip check" in install
|
||||
assert not re.search(r"pip install.*funasr[>=]", install)
|
||||
|
||||
|
||||
def test_fun_asr_nano_finetune_zh_uses_canonical_filename():
|
||||
docs_dir = ROOT / "examples/industrial_data_pretraining/fun_asr_nano/docs"
|
||||
assert (docs_dir / "finetune_zh.md").exists()
|
||||
|
||||
@@ -4,6 +4,8 @@ Run with Sphinx and the extensions listed in docs/conf.py installed.
|
||||
"""
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -15,6 +17,40 @@ import pytest
|
||||
pytest.importorskip('sphinx')
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_vllm_compatibility_pages_preserve_rendered_section_targets(tmp_path):
|
||||
docs = tmp_path / 'docs'
|
||||
docs.mkdir()
|
||||
shutil.copy2(ROOT / 'docs/conf.py', docs / 'conf.py')
|
||||
shutil.copytree(ROOT / 'docs/_ext', docs / '_ext')
|
||||
names = ('vllm_guide_zh', 'vllm_guide_zh_v2')
|
||||
(docs / 'index.rst').write_text(
|
||||
'Guides\n======\n\n.. toctree::\n\n vllm_guide_zh\n vllm_guide_zh_v2\n')
|
||||
for name in names:
|
||||
shutil.copy2(ROOT / 'docs' / (name + '.md'), docs / (name + '.md'))
|
||||
output = tmp_path / 'html'
|
||||
result = subprocess.run(
|
||||
[sys.executable, '-m', 'sphinx', '-b', 'html', str(docs), str(output)],
|
||||
text=True, capture_output=True, timeout=90)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
for name in names:
|
||||
soup = BeautifulSoup((output / (name + '.html')).read_text(), 'html.parser')
|
||||
article = soup.select_one('[role=main]')
|
||||
pairs = []
|
||||
for section in article.select('section[id],div.section[id]'):
|
||||
heading = section.find(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], recursive=False)
|
||||
ids = [section['id']] + [
|
||||
node['id'] for node in section.find_all(id=True, recursive=False)
|
||||
if node.name == 'span']
|
||||
pairs.append([ids, heading.get_text() if heading else None])
|
||||
# Published pre-consolidation Sphinx output: 40 heading/target pairs,
|
||||
# including numeric ids and the duplicate Chinese heading alias.
|
||||
assert len(pairs) == 40
|
||||
digest = hashlib.sha256(json.dumps(pairs, ensure_ascii=False).encode()).hexdigest()
|
||||
assert digest == 'fe2775e9be07ba6a8bc7cb899a41fe7441ca9d1710ec7b7aee425dfff26a095e', name
|
||||
mirror = BeautifulSoup((output / 'vllm_guide_zh_v2.html').read_text(), 'html.parser')
|
||||
assert mirror.select_one('a[href="vllm_guide_zh.html"]')
|
||||
|
||||
GUIDE = '''# Link fixture
|
||||
|
||||
[Body code](../../examples/demo.py)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Exercise the compatibility guide generator through its real CLI."""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts/sync_vllm_guide.py"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def guide_tree(tmp_path):
|
||||
assert SCRIPT.is_file(), "The compatibility guide needs a sync CLI"
|
||||
(tmp_path / "scripts").mkdir()
|
||||
(tmp_path / "docs").mkdir()
|
||||
shutil.copy2(SCRIPT, tmp_path / "scripts" / SCRIPT.name)
|
||||
source = tmp_path / "docs/vllm_guide_zh.md"
|
||||
source.write_bytes(b"# Guide\n\n## Section\n\n[Local](./api.md)\n")
|
||||
return tmp_path, source, tmp_path / "docs/vllm_guide_zh_v2.md"
|
||||
|
||||
|
||||
def run_sync(root, *args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(root / "scripts" / SCRIPT.name), *args],
|
||||
cwd=root.parent,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def test_generate_full_guide_and_repeat_without_changing_source(guide_tree):
|
||||
root, source, target = guide_tree
|
||||
original = source.read_bytes()
|
||||
result = run_sync(root)
|
||||
assert result.returncode == 0, result.stderr
|
||||
generated = target.read_bytes()
|
||||
assert generated.endswith(original)
|
||||
assert b"[" in generated[: -len(original)]
|
||||
assert b"vllm_guide_zh.md" in generated[: -len(original)]
|
||||
assert run_sync(root).returncode == 0
|
||||
assert target.read_bytes() == generated
|
||||
assert source.read_bytes() == original
|
||||
assert run_sync(root, "--check").returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["missing", "target", "source", "line-endings"])
|
||||
def test_check_detects_drift_without_writing(guide_tree, change):
|
||||
root, source, target = guide_tree
|
||||
assert run_sync(root).returncode == 0
|
||||
if change == "missing":
|
||||
target.unlink()
|
||||
elif change == "target":
|
||||
target.write_bytes(target.read_bytes() + b"\nManual edit\n")
|
||||
elif change == "source":
|
||||
source.write_bytes(source.read_bytes() + b"\nNew section\n")
|
||||
else:
|
||||
target.write_bytes(target.read_bytes().replace(b"\n", b"\r\n"))
|
||||
before = {p.name: p.read_bytes() for p in (source, target) if p.exists()}
|
||||
result = run_sync(root, "--check")
|
||||
assert result.returncode == 1
|
||||
assert "sync_vllm_guide.py" in result.stderr
|
||||
after = {p.name: p.read_bytes() for p in (source, target) if p.exists()}
|
||||
assert before == after
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [(), ("--check",)])
|
||||
def test_missing_source_does_not_replace_existing_mirror(guide_tree, args):
|
||||
root, source, target = guide_tree
|
||||
target.write_bytes(b"Existing compatibility page\n")
|
||||
source.unlink()
|
||||
result = run_sync(root, *args)
|
||||
assert result.returncode == 1
|
||||
assert "vllm_guide_zh.md" in result.stderr
|
||||
assert target.read_bytes() == b"Existing compatibility page\n"
|
||||
|
||||
|
||||
def test_repository_compatibility_guide_is_current():
|
||||
result = run_sync(ROOT, "--check")
|
||||
assert result.returncode == 0, result.stderr
|
||||
@@ -7,7 +7,6 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
VLLM_GUIDES = [
|
||||
"docs/vllm_guide.md",
|
||||
"docs/vllm_guide_zh.md",
|
||||
"docs/vllm_guide_zh_v2.md",
|
||||
]
|
||||
|
||||
|
||||
@@ -31,20 +30,20 @@ def test_vllm_guides_distinguish_official_and_native_model_paths(relpath):
|
||||
assert marker in text, f"{relpath} is missing {marker}"
|
||||
|
||||
|
||||
def test_v2_guide_keeps_native_vllm_and_funasr_realtime_paths_separate():
|
||||
text = (ROOT / "docs/vllm_guide_zh_v2.md").read_text(encoding="utf-8")
|
||||
def test_primary_guide_keeps_native_vllm_and_funasr_realtime_paths_separate():
|
||||
text = (ROOT / "docs/vllm_guide_zh.md").read_text(encoding="utf-8")
|
||||
|
||||
required_markers = [
|
||||
"FunAudioLLM/Fun-ASR-Nano-2512-vllm",
|
||||
"vllm serve FunAudioLLM/Fun-ASR-Nano-2512-vllm --port 8000",
|
||||
"vllm_official_native_validation_zh.md",
|
||||
"serve_realtime_ws.py",
|
||||
"realtime speech-to-text",
|
||||
"没有可用的实时转写会话",
|
||||
"VAD、partial",
|
||||
"不会注册 `/v1/realtime`",
|
||||
"不是稳定的 API 契约",
|
||||
]
|
||||
|
||||
for marker in required_markers:
|
||||
assert marker in text, f"docs/vllm_guide_zh_v2.md is missing {marker}"
|
||||
assert marker in text, f"docs/vllm_guide_zh.md is missing {marker}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -157,7 +157,7 @@ td, th { padding: 14px; }
|
||||
.docs-article pre { position: relative; overflow-x: auto; padding: 24px 50px 24px 20px; margin: 20px 0; max-width: 100%; font-size: 13px; line-height: 1.8; }
|
||||
.docs-article pre code { background: none; color: inherit; white-space: pre; padding: 0; }
|
||||
.docs-article :not(pre) > code { padding: 2px 5px; border-radius: 4px; background: #f0f4f1; font-size: .88em; }
|
||||
.docs-article table { display: block; overflow-x: auto; max-width: 100%; width: 100%; }
|
||||
.docs-article table { display: block; overflow-x: auto; max-width: 100%; width: 100%; overflow-wrap: normal; }
|
||||
.docs-article blockquote { border-left: 3px solid #7ea98e; background: #f3f8f4; margin: 20px 0; padding: 8px 20px; color: #52655a; }
|
||||
.docs-toc { position: sticky; top: 104px; max-height: calc(100vh - 130px); overflow-y: auto; border-left: 1px solid var(--line); padding-left: 18px; font-size: 12px; }
|
||||
.docs-toc strong { color: var(--ink); }
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
for (const width of [390, 1440]) {
|
||||
for (const prefix of ['', '/en']) {
|
||||
test(`vLLM benchmark cells stay readable at ${width} ${prefix || '/zh'}`, async ({ page }, testInfo) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto(`${prefix}/docs/vllm.html`);
|
||||
const table = page.locator('.docs-article table').first();
|
||||
await expect(table).toBeVisible();
|
||||
const cells = table.locator('th').filter({ hasText: /^(RTFx|CER)$/ });
|
||||
await expect(cells).toHaveCount(2);
|
||||
for (const cell of await cells.all()) {
|
||||
const metrics = await cell.evaluate(node => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
return {
|
||||
textHeight: range.getBoundingClientRect().height,
|
||||
fontSize: parseFloat(getComputedStyle(node).fontSize),
|
||||
};
|
||||
});
|
||||
expect(metrics.textHeight).toBeLessThan(metrics.fontSize * 1.8);
|
||||
}
|
||||
if (width === 390) {
|
||||
const scroll = await table.evaluate(node => {
|
||||
node.scrollLeft = node.scrollWidth;
|
||||
return { left: node.scrollLeft, width: node.clientWidth, content: node.scrollWidth };
|
||||
});
|
||||
expect(scroll.content).toBeGreaterThan(scroll.width);
|
||||
expect(scroll.left).toBeGreaterThan(0);
|
||||
}
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
|
||||
await page.screenshot({ path: testInfo.outputPath('readable-table.png') });
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user