本文记录 M2M100 翻译模型在 Ascend NPU 上的 WMT19 精度验证、Hugging Face Transformers 推理性能、FastAPI 服务化部署、API 调用、服务性能测试,以及长文本翻译处理方式。
其中 HF 推理指使用 Hugging Face Transformers 直接加载模型并在 NPU 上执行 generate()。
本次验证覆盖两类模型:
| 模型 | 用途 | 模型来源 |
|---|---|---|
| M2M100 418M | 精度基线验证、HF 性能测试 | https://huggingface.co/facebook/m2m100_418M |
| M2M100 1.2B | 精度补充验证、HF 性能测试、服务化验证 | https://huggingface.co/facebook/m2m100_1.2B |
设备信息:
| 项目 | 规格 |
|---|---|
| 服务器 | Atlas 800T A2 |
| 加速卡 | Ascend 910B3 |
| 项目 | 版本 / 规格 |
|---|---|
| 操作系统 / 架构 | Ubuntu 22.04.5 LTS / aarch64 |
| 驱动 / 固件 | 25.2.0 / 7.0.3.220 |
| CANN | 8.5.1 |
| Python | 3.11.14 |
| torch / torch_npu | 2.9.0+cpu / 2.9.0.post1+gitee7ba04 |
| 占位符 | 含义 |
|---|---|
<PROJECT_ROOT> | 本项目根目录 |
<MODEL_ROOT> | 模型目录根路径 |
<FAIRSEQ_ROOT> | 已完成 M2M100 适配的 fairseq 目录 |
<RUN_DIR> | 精度或性能测试输出目录 |
<SERVICE_ENDPOINT> | FastAPI 服务地址 |
服务启动、API 调用和 HF direct 性能测试只依赖当前项目代码与 Hugging Face 模型目录,不需要 fairseq 代码仓。只有运行 WMT19 精度评测时,才需要准备 <FAIRSEQ_ROOT>。
如果本地还没有 fairseq 代码仓,可以先克隆代码并切换到项目使用的 fairseq 目录:
git clone https://github.com/facebookresearch/fairseq.git <FAIRSEQ_ROOT>
cd <FAIRSEQ_ROOT><FAIRSEQ_ROOT> 需要包含完整的 M2M100 tokenizer 资源,至少应存在:
<FAIRSEQ_ROOT>/examples/m2m_100/tokenizers/thirdparty/mosesdecoder/scripts/tokenizer/tokenizer.perl该文件来自 Moses SMT 的 mosesdecoder 仓库。若当前 fairseq 目录缺少该文件,在 M2M100 示例目录执行:
cd <FAIRSEQ_ROOT>/examples/m2m_100
bash install_dependecies.sh该脚本会将 Moses tokenizer 下载到 tokenizers/thirdparty/mosesdecoder,并固定到 M2M100 示例使用的 revision。
评测方向:
zh -> en评测数据:
| 项目 | 内容 |
|---|---|
| 数据集 | WMT19 Chinese-English |
| split | wmt19 |
| 语言对 | zh-en |
| 样本数 | 2000 |
| 数据获取 | python -m sacrebleu -t wmt19 -l zh-en --echo src/ref |
| 指标 | detokenized BLEU |
| 计分方式 | tok.sh en 后使用 sacrebleu -tok none |
| beam | 5 |
| 最大生成长度 | 200 |
这里的计分方式与 M2M100 README 计分口径保持一致:先使用 M2M100 示例目录中的 tok.sh en 对 hypothesis 和 reference 做英文 tokenization,再使用 sacrebleu -tok none 计分。
论文 Table 10 中 Chinese-English WMT’19 结果:
| 系统 | BLEU |
|---|---|
| Published best single model | 29.1 |
| M2M-100 | 29.0 |
论文说明 Table 10 使用 WMT test set 上的 detokenized BLEU,并使用 sacreBLEU 计分。
418M 与 1.2B 使用同一个 HF 精度评测入口:
hf_m2m100_api/evaluate_hf_wmt19.py该脚本在未指定 --devices 时使用 --device 做单卡生成;指定 --devices 时会自动按行号切分 WMT19 样本、多进程加载模型、每卡生成一个 shard,最后按原始顺序合并 hypothesis 并统一计分。
为了缩短全量评测时间,实际采用 8 卡分片生成,每张卡处理连续 250 条样本:
cd <PROJECT_ROOT>
python hf_m2m100_api/evaluate_hf_wmt19.py \
--model-dir <MODEL_ROOT>/m2m100_418M \
--fairseq-dir <FAIRSEQ_ROOT> \
--output-dir <RUN_DIR>/wmt19-zh-en-hf-418m-sharded \
--devices npu:0,npu:1,npu:2,npu:3,npu:4,npu:5,npu:6,npu:7 \
--source-lang zh \
--target-lang en \
--beam 5 \
--batch-size 1 \
--max-new-tokens 200实测结果:
| 模型 | 链路 | 样本数 | BLEU | 与官方 M2M-100 29.0 差值 | 设备 | wall time |
|---|---|---|---|---|---|---|
| M2M100 418M | HF generate sharded | 2000 | 22.6 | -6.4 | 8 x NPU | 319.259 s |
418M 与论文 M2M-100 结果差距较大。该差异符合预期:论文 Table 10 的 M2M-100 并不是 418M 小模型的公开精度基线,而是更大容量的 M2M-100 系统结果。因此继续验证 1.2B 模型。
1.2B 使用同一脚本进行完整 WMT19 zh-en 评测,只需要替换模型目录与输出目录:
cd <PROJECT_ROOT>
python hf_m2m100_api/evaluate_hf_wmt19.py \
--model-dir <MODEL_ROOT>/m2m100_1.2B \
--fairseq-dir <FAIRSEQ_ROOT> \
--output-dir <RUN_DIR>/wmt19-zh-en-hf-1.2b \
--devices npu:0,npu:1,npu:2,npu:3,npu:4,npu:5,npu:6,npu:7 \
--source-lang zh \
--target-lang en \
--beam 5 \
--batch-size 1 \
--max-new-tokens 200实测结果:
| 模型 | 链路 | 样本数 | BLEU | 与官方 M2M-100 29.0 差值 |
|---|---|---|---|---|
| M2M100 1.2B | HF generate | 2000 | 27.8 | -1.2 |
1.2B 结果明显接近论文 Table 10 的 M2M-100 29.0,比 418M 高 5.2 BLEU。后续服务化部署选择 1.2B 作为主服务模型。
| 模型 | BLEU | 结论 |
|---|---|---|
| 论文 M2M-100 | 29.0 | 官方 Chinese-English WMT’19 参考值。 |
| M2M100 418M | 22.6 | 与官方差距 6.4 BLEU,不适合作为官方精度复现模型。 |
| M2M100 1.2B | 27.8 | 与官方差距 1.2 BLEU,适合作为服务化主模型。 |
需要注意:论文结果、HF 418M 路径和 HF 1.2B 路径在模型容量、实现细节、tokenization 和 checkpoint 来源上不完全一致,因此这里存在一定差异。
短句性能测试使用:
New Delhi is the capital of India.公共参数:
| 项目 | 值 |
|---|---|
| 翻译方向 | en -> zh |
| beam | 5 |
| max new tokens | 128 |
| batch size | 1 |
| warmup | 3 |
| iterations | 20 |
natural 表示按 tokenizer 输出的真实输入长度进行推理。
cd <PROJECT_ROOT>
python hf_m2m100_api/benchmark_hf_pad_compare.py \
--model-dir <MODEL_ROOT>/m2m100_418M \
--device npu:1 \
--warmup 3 \
--iterations 20| 模型 | 输入 shape | generate only 平均耗时 | tokenize+generate 平均耗时 | P50 generate |
|---|---|---|---|---|
| 418M | [1,10] | 0.2324 s | 0.2332 s | 0.2336 s |
1.2B 使用相同短句测试输入进行 direct HF 性能测试:
cd <PROJECT_ROOT>
python hf_m2m100_api/benchmark_hf_pad_compare.py \
--model-dir <MODEL_ROOT>/m2m100_1.2B \
--device npu:1 \
--warmup 3 \
--iterations 20实测结果:
| 模型 | 输入 shape | generate only 平均耗时 | tokenize+generate 平均耗时 | P50 generate |
|---|---|---|---|---|
| 418M | [1,10] | 0.2324 s | 0.2332 s | 0.2336 s |
| 1.2B | [1,10] | 0.4162 s | 0.4171 s | 0.4155 s |
1.2B 相比 418M 具备更好的 WMT19 zh-en 精度,但短句 direct 推理延迟约为 418M 的 1.8 倍。
<PROJECT_ROOT>/
hf_m2m100_api/
hf_m2m100_fastapi.py
hf_m2m100_npu_infer.py
text_splitter.py
evaluate_hf_wmt19.py
benchmark_api.py
benchmark_hf_concurrency.py
benchmark_hf_multiprocess.py
benchmark_hf_pad_compare.py
requirements.txt
start.sh
tests/
test_benchmark_hf_pad_compare.py
test_hf_m2m100_fastapi.py
test_hf_m2m100_npu_infer.py
test_start_sh.py
test_text_splitter.py
pytest.ini模块职责:
| 文件 | 说明 |
|---|---|
hf_m2m100_api/hf_m2m100_fastapi.py | FastAPI 应用、服务生命周期、请求校验和同步推理锁。 |
hf_m2m100_api/hf_m2m100_npu_infer.py | HF 模型加载、设备解析、复用已加载模型实例推理。 |
hf_m2m100_api/text_splitter.py | 长文本句子切分、超长单句兜底拆分、翻译 chunk 并按原输入拼回。 |
hf_m2m100_api/evaluate_hf_wmt19.py | WMT19 zh-en HF 精度评测,覆盖数据获取、单卡或多卡分片生成、分词和 sacrebleu 计分。 |
hf_m2m100_api/benchmark_api.py | HTTP 服务端到端性能测试。 |
hf_m2m100_api/benchmark_hf_concurrency.py | 不经过 HTTP 的 HF 多线程并发测试。 |
hf_m2m100_api/benchmark_hf_multiprocess.py | 不经过 HTTP 的 HF 多进程并发测试。 |
hf_m2m100_api/benchmark_hf_pad_compare.py | 不经过 HTTP 的 HF direct 短句性能测试。 |
tests/ | HF 服务、HF 推理、长文本拆分和 benchmark 参数构造的回归测试。 |
代码验证:
cd <PROJECT_ROOT>
python -m py_compile hf_m2m100_api/*.py
bash -n hf_m2m100_api/start.sh
pytest -qcd <PROJECT_ROOT>/hf_m2m100_api
pip install -r requirements.txtcd <PROJECT_ROOT>/hf_m2m100_api
M2M100_MODEL_DIR=<MODEL_ROOT>/m2m100_1.2B \
M2M100_DEVICE=npu:1 \
M2M100_HOST=0.0.0.0 \
M2M100_PORT=8000 \
M2M100_WORKERS=1 \
./start.sh等价 uvicorn 命令:
M2M100_MODEL_DIR=<MODEL_ROOT>/m2m100_1.2B \
M2M100_DEVICE=npu:1 \
uvicorn hf_m2m100_fastapi:app \
--host 0.0.0.0 \
--port 8000 \
--workers 1启动参数:
| 参数 | 作用 |
|---|---|
M2M100_MODEL_DIR / --model-dir | 指定 HF 模型目录。 |
M2M100_DEVICE / --device | 指定模型加载和推理使用的 NPU。 |
M2M100_HOST / --host | 指定 HTTP 监听地址。 |
M2M100_PORT / --port | 指定 HTTP 监听端口。 |
M2M100_WORKERS / --workers | 指定 uvicorn worker 进程数。每个 worker 会独立加载一份模型实例。 |
健康检查:
curl http://<SERVICE_ENDPOINT>/health翻译请求:
curl -X POST http://<SERVICE_ENDPOINT>/translate \
-H 'Content-Type: application/json' \
-d '{
"texts": ["New Delhi is the capital of India."],
"src_lang": "en",
"tgt_lang": "zh"
}'请求参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
texts | 无 | 待翻译文本列表。 |
src_lang | 无 | 源语言代码,调用时需要显式填写。完整语言列表可查看模型目录下的 special_tokens_map.json,字段为 additional_special_tokens;请求参数使用去掉前后双下划线后的代码,例如 __zh__ 对应 zh。 |
tgt_lang | 无 | 目标语言代码,调用时需要显式填写。该参数用于指定模型输出哪种语言。完整语言列表可查看模型目录下的 special_tokens_map.json,字段为 additional_special_tokens;请求参数使用去掉前后双下划线后的代码,例如 __en__ 对应 en。 |
num_beams | 5 | beam search 的候选路径数量。值越大,生成时保留并比较的候选翻译越多,通常有利于翻译质量,但会增加推理耗时。 |
split_long_text | true | 是否启用服务端长文本拆分。开启后服务按句子边界拆分输入文本,再按原始顺序拼回翻译结果。 |
max_input_tokens_per_chunk | 100 | 单个句子过长时的兜底拆分长度。正常情况下服务按句子切分,一句作为一个 chunk;只有某一句本身超过模型可接受的输入长度时,才会按该值继续拆小。 |
请求示例:
curl -X POST http://<SERVICE_ENDPOINT>/translate \
-H 'Content-Type: application/json' \
-d '{
"texts": ["New Delhi is the capital of India."],
"src_lang": "en",
"tgt_lang": "zh"
}'响应格式:
{
"translations": ["新德里是印度的首都。"],
"src_lang": "en",
"tgt_lang": "zh",
"elapsed_seconds": 0.4092
}M2M100 的 HF generate() 可以接收接近模型上限的长输入,但模型训练和常规测试数据通常按单条语句组织。直接把多句或整段长文本作为一条输入时,实测容易出现漏翻、重复或提前结束。这里不是服务接口把输入截短了,而是模型把多句长文本当成一条很长的句子翻译时,生成过程可能提前认为已经翻译完成,导致后面的内容被跳过,或者前面的内容被重复生成。
服务端处理策略:
split_long_text=true。max_input_tokens_per_chunk 做兜底硬拆,避免请求超过 tokenizer/model 可处理范围。服务 benchmark:
cd <PROJECT_ROOT>
python hf_m2m100_api/benchmark_api.py \
--url http://<SERVICE_ENDPOINT> \
--warmup 3 \
--iterations 20服务实测:
| 模型 | 链路 | client end-to-end 平均耗时 | service elapsed 平均耗时 | P50 client | P50 service |
|---|---|---|---|---|---|
| 418M | FastAPI 预加载模型推理 | 0.2464 s | 0.2439 s | 0.2469 s | 0.2445 s |
| 1.2B | FastAPI 预加载模型推理 | 0.4323 s | 0.4294 s | 0.4336 s | 0.4306 s |
当前服务默认启用长文本拆分。短句请求只会拆出一个句子 chunk,因此端到端耗时仍接近 direct HF natural;1.2B 的 HTTP 端到端平均耗时为 0.4323 s,主要延迟来自模型生成。
当前 start.sh 默认启动单个服务进程。如果希望在同一张 NPU 上启动多个模型实例,可以通过 M2M100_WORKERS 或直接向 start.sh 传入 --workers:
cd <PROJECT_ROOT>/hf_m2m100_api
M2M100_MODEL_DIR=<MODEL_ROOT>/m2m100_1.2B \
M2M100_DEVICE=npu:1 \
M2M100_HOST=127.0.0.1 \
M2M100_PORT=18183 \
M2M100_WORKERS=<WORKERS> \
./start.sh每个 worker 是一个独立进程,会各自加载一份模型实例到同一张 NPU。该方式可以提高短句并发吞吐,但显存占用会随 worker 数增加,单请求延迟也可能随并发数升高。
测试口径:
| 项目 | 值 |
|---|---|
| 模型 | M2M100 1.2B |
| 设备 | 单张 Ascend 910B3 |
| 请求文本 | New Delhi is the capital of India. |
| 源语言 / 目标语言 | en -> zh |
| beam | 5 |
| 最大生成长度 | 128 |
| 每档正式请求数 | 40 |
| 每档预热请求数 | 4 |
实测结果:
| workers | 并发数 | QPS | client 平均耗时 | client P95 | service 平均耗时 | service P95 |
|---|---|---|---|---|---|---|
| 1 | 1 | 2.26 | 0.4423 s | 0.4560 s | 0.4397 s | 0.4533 s |
| 1 | 2 | 2.29 | 0.6556 s | 0.8832 s | 0.6512 s | 0.8788 s |
| 1 | 4 | 2.24 | 1.1188 s | 1.8058 s | 1.1123 s | 1.7998 s |
| 1 | 8 | 2.26 | 1.9866 s | 3.5171 s | 1.9767 s | 3.5098 s |
| 2 | 1 | 2.26 | 0.4411 s | 0.4481 s | 0.4384 s | 0.4453 s |
| 2 | 2 | 4.47 | 0.4413 s | 0.4577 s | 0.4383 s | 0.4548 s |
| 2 | 4 | 4.10 | 0.6803 s | 0.9031 s | 0.6754 s | 0.8952 s |
| 2 | 8 | 3.90 | 1.1238 s | 2.1623 s | 1.1157 s | 2.1544 s |
| 3 | 1 | 2.13 | 0.4684 s | 0.4606 s | 0.4654 s | 0.4578 s |
| 3 | 2 | 4.45 | 0.4371 s | 0.4917 s | 0.4342 s | 0.4886 s |
| 3 | 4 | 4.59 | 0.5426 s | 0.8877 s | 0.5383 s | 0.8825 s |
| 3 | 8 | 5.21 | 0.8441 s | 1.4382 s | 0.8374 s | 1.4339 s |
| 4 | 1 | 2.10 | 0.4753 s | 0.4707 s | 0.4727 s | 0.4685 s |
| 4 | 2 | 4.15 | 0.4535 s | 0.4525 s | 0.4506 s | 0.4499 s |
| 4 | 4 | 6.41 | 0.5034 s | 0.9180 s | 0.4997 s | 0.9113 s |
| 4 | 8 | 5.96 | 0.7475 s | 1.3605 s | 0.7421 s | 1.3548 s |
测试结果显示,单 worker 下提高并发数不会提升吞吐,只会增加排队延迟;多 worker 会在同卡上加载多个模型实例,可以提升短句并发吞吐。