前言

本文记录一次在 Atlas 800I A2(8×Ascend 910B3,单卡 64 GB HBM) 上部署 MiniMax-H3 的完整过程。

目标是使用 vLLM-Omni 提供 T2VA(文生视频+音频)和 FL2VA(首帧/首尾帧生视频+音频)服务,输出 1344×768、24 FPS 的视频。过程中主要遇到了三类问题:

  1. 8 张 64 GB NPU 仍然在 15 秒、1344×768 请求上 OOM;
  2. 同步接口长时间等待,服务端已经生成完成,客户端却收不到 MP4;
  3. 同时提交两三个任务时看似“假死”,但 NPU Core 使用率一直很高。

最终结果:15 秒、1344×768、24 FPS、50 steps 的 T2VA 请求成功完成,单卡峰值 HBM 约 46 GB,未再触发 OOM;同时将客户端切换为异步任务接口,解决了长连接断开后结果无法回传的问题。

1. 参考资料

2. 环境与模型目录

本次验证环境:

项目 配置
服务器 Atlas 800I A2
NPU 8×Ascend 910B3,64 GB HBM/卡
驱动 26.0.rc1
PyTorch 2.10.0+cpu
vLLM-Omni 0.26 分支 + 已合入上游的 MiniMax-H3 修复
推理模式 USP8、Text Encoder TP8、VAE Patch Parallel 8
显存优化 Distributed Layerwise Offload(DLO)
注意力 RainFusion,后续合入 packed-varlen 修复

T2VA 与 FL2VA 共享同一份模型权重,Ref2VA 使用另一份权重。示例目录如下:

/data/model/MiniMax-H3/
├── FL2VA
└── Ref2VA

本文启动的是 T2VA/FL2VA 服务:

export MODEL=/data/model/MiniMax-H3/FL2VA

如果需要 Ref2VA,使用同一套容器参数,将 MODEL 改成 /data/model/MiniMax-H3/Ref2VA 即可。

3. 安装 MindIE-SD 算子依赖

Ascend 上建议安装 MindIE-SD,以获得 MiniMax-H3 所需的融合算子和 RainFusion 注意力实现。以下流程来自公开参考文档:

git clone https://gitcode.com/Ascend/MindIE-SD.git
cd MindIE-SD

# 当前场景不需要 tik_ops 时,可按官方 NPU recipe 跳过该构建步骤
sed -i 's|^\(\s*\)source ${current_script_dir}/build_tik_ops.sh|\1# source ${current_script_dir}/build_tik_ops.sh|' build/build_ops.sh

python setup.py bdist_wheel
pip install dist/mindiesd-*.whl

如果环境必须通过代理访问外网,可通过环境变量或企业统一配置注入代理地址:

export http_proxy=http://proxy.example.com:PORT
export https_proxy=http://proxy.example.com:PORT

4. 8 卡后台启动脚本

下面是经过验证的启动结构。启动前设置实际镜像名,并根据主机内存调整 --shm-size

#!/usr/bin/env bash
set -euo pipefail

NAME=minimax-h3-fl2va
IMAGE=your-registry/vllm-omni-minimax-h3:latest
MODEL=/data/model/MiniMax-H3/FL2VA
VIDEO_STORAGE=/data/vllm-omni-videos

mkdir -p "${VIDEO_STORAGE}"
docker rm -f "${NAME}" >/dev/null 2>&1 || true

docker run -d \
  --name "${NAME}" \
  --restart unless-stopped \
  --shm-size=128g \
  --net=host \
  --privileged=true \
  --device /dev/davinci0 \
  --device /dev/davinci1 \
  --device /dev/davinci2 \
  --device /dev/davinci3 \
  --device /dev/davinci4 \
  --device /dev/davinci5 \
  --device /dev/davinci6 \
  --device /dev/davinci7 \
  --device /dev/davinci_manager \
  --device /dev/devmm_svm \
  --device /dev/hisi_hdc \
  -v /data/model:/data/model \
  -v "${VIDEO_STORAGE}:${VIDEO_STORAGE}" \
  -v /usr/local/dcmi:/usr/local/dcmi \
  -v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool \
  -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
  -v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
  -v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
  -v /etc/ascend_install.info:/etc/ascend_install.info \
  -v /etc/hccn.conf:/etc/hccn.conf \
  -v /root/.cache:/root/.cache \
  -e ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \
  -e VLLM_WORKER_MULTIPROC_METHOD=spawn \
  -e VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
  -e VLLM_OMNI_SERVER_STORAGE__PATH="${VIDEO_STORAGE}" \
  -e PYTHONDONTWRITEBYTECODE=1 \
  -e HF_HUB_OFFLINE=1 \
  -e TRANSFORMERS_OFFLINE=1 \
  -e PYTORCH_NPU_ALLOC_CONF=expandable_segments:True \
  -e HCCL_NPU_SOCKET_PORT_RANGE=auto \
  "${IMAGE}" \
  bash -lc "source /usr/local/Ascend/ascend-toolkit/set_env.sh && exec vllm serve '${MODEL}' \
    --omni \
    --host 0.0.0.0 \
    --port 9098 \
    --trust-remote-code \
    --num-gpus 8 \
    --usp 8 \
    --ring 1 \
    --text-encoder-tp-size 8 \
    --enable-distributed-layerwise-offload \
    --vae-parallel-mode tile \
    --vae-use-tiling \
    --vae-patch-parallel-size 8 \
    --enable-diffusion-pipeline-profiler \
    --diffusion-attention-config '{\"default\":{\"backend\":\"RAINFUSION_ATTN\",\"block_sparse\":{\"sparsity\":0.8,\"start_step\":12}}}'"

docker run -d 表示后台运行,关闭 SSH 窗口不会停止服务。--restart unless-stopped 会在容器异常退出或服务器重启后自动拉起;但如果手动执行 docker stop,容器不会立即自动启动。

常用检查命令:

docker ps --filter name=minimax-h3-fl2va
docker logs -f minimax-h3-fl2va
curl -f http://127.0.0.1:9098/health
npu-smi info

5. 问题一:8 张 64 GB NPU 为什么仍然 OOM

最初的 15 秒、1344×768 请求在注意力阶段失败。报错显示某个 rank 还要一次性申请约 44.78 GiB,但该卡当时只剩约 24 GiB。

关键点是:8 卡总显存足够,不等于任意一次 rank-local 临时分配都能成功。

旧实现会执行类似操作:

full_mask = attn_mask.expand(batch, heads, q_len, kv_len).contiguous()

这会在每个 rank 上物化一个近似 Sq × Skv 的完整注意力 mask。序列并行虽然分摊了模型和部分计算,但这个巨型连续临时张量仍然是单卡分配,因此总显存相加没有意义。

修复方式:packed-varlen mask-free attention

vLLM-Omni 上游 PR #5891 为 MiniMax-H3 的 NPU 路径增加了 packed-varlen attention:

  • 使用 cu_seqlensmax_seqlen 描述有效 token;
  • 支持 K/V 有效前缀切片;
  • 不再物化完整的二次方 padding mask;
  • 保留不支持该能力的 attention backend 的安全回退路径。

如果使用较新的 vLLM-Omni 主线,该修复已经包含在内。旧版本需要升级,或在严格核对版本差异后 backport 上游提交:

git cherry-pick 740f45abcd2e6754d1cdded01ee9901d595ccac5

不建议只删除 mask:如果 backend 仍会读取 padding token,可能得到静默错误。正确做法是由 backend 显式声明 mask-free packed 能力,并通过契约测试验证。

修复后的结果

单任务验证参数:

任务:T2VA
时长:15 秒
分辨率:1344×768
帧率:24 FPS
采样步数:50
并行:USP8 + Text Encoder TP8 + VAE Patch Parallel 8
注意力:RainFusion,sparsity=0.8,start_step=12

实测结果:

  • 推理成功,无 OOM;
  • 单卡峰值 HBM 约 46,973 MB;
  • 客户端总耗时约 781 秒(约 13 分钟);
  • 输出为 H.264 1344×768、24 FPS,带 AAC 32 kHz 立体声音频。

这些数字只代表当前软硬件组合,不应当视作所有 A2/A3 服务器的统一性能基线。

6. 问题二:同步请求已生成完成,客户端为什么还一直等待

POST /v1/videos/sync 会一直保持 HTTP 连接,直到生成、编码和整个 MP4 返回完成。长达十几分钟的连接容易受到 VPN、代理、客户端超时和网络切换影响。

一次排查中,服务端日志已经出现 200 OK,但 TCP 连接仍有较大的 Send-Q,持续重传且客户端不再 ACK。此时 NPU 推理已经结束,真正卡住的是 MP4 回传链路。

推荐:使用异步视频任务 API

vLLM-Omni 已经提供完整的异步接口:

接口 作用
POST /v1/videos 创建任务,立即返回任务 ID
GET /v1/videos/{video_id} 查询状态和元数据
GET /v1/videos/{video_id}/content 下载已完成的 MP4
GET /v1/videos 列出当前服务进程保存的任务
DELETE /v1/videos/{video_id} 删除任务及其输出

提交任务:

BASE_URL=http://your-server.example.com:9098

create_response=$(curl -sS -X POST "${BASE_URL}/v1/videos" \
  -F 'prompt=A camper van drives along a wet alpine road at dawn.' \
  -F 'width=1344' \
  -F 'height=768' \
  -F 'aspect_ratio=16:9' \
  -F 'fps=24' \
  -F 'num_inference_steps=50' \
  -F 'flow_shift=12' \
  -F 'seed=1101' \
  -F 'extra_params={"task":"t2va","duration":8.7,"audio_flow_shift":3.0}')

video_id=$(echo "${create_response}" | jq -r '.id')
echo "video_id=${video_id}"

轮询并下载:

while true; do
  status_response=$(curl -sS "${BASE_URL}/v1/videos/${video_id}")
  status=$(echo "${status_response}" | jq -r '.status')
  echo "status=${status}"

  case "${status}" in
    queued|in_progress) sleep 5 ;;
    completed) break ;;
    failed) echo "${status_response}" | jq .; exit 1 ;;
    *) echo "unexpected status"; exit 1 ;;
  esac
done

curl -fL "${BASE_URL}/v1/videos/${video_id}/content" -o output.mp4

客户端退出后,只要保存了 video_id,重新连接即可继续查询,不会重复占用 NPU 提交同一任务。

需要注意:当前普通 H3 request-execution 模式下,progress 字段实际上通常只会从 0 变成 100,应以 queuedin_progresscompletedfailed 状态为准。另外,任务元数据目前保存在服务进程内存中;服务重启后 API 元数据可能丢失,因此还需要把视频文件目录挂载到宿主机持久化路径。

export VLLM_OMNI_SERVER_STORAGE__PATH=/data/vllm-omni-videos

7. 问题三:两三个任务一起提交,为什么感觉全部假死

MiniMax-H3 当前服务配置中,一个生成请求就会占满 8 张卡进行 USP8 推理。观察到单任务运行时 NPU Core 使用率经常达到 74%~100%,因此客户端并发不会把同一组 8 卡自动变成多路并行服务。

实际表现通常是:

  • 1 个请求正在运行;
  • 其余请求在调度队列等待;
  • 每个客户端都显示“等待”,于是看起来像所有请求都卡住;
  • 后续任务的端到端耗时包含前面任务的排队时间。

通过 metrics 判断运行与排队数量:

curl -s http://127.0.0.1:9098/metrics | \
  grep -E 'vllm_omni:(num_requests_running|num_requests_waiting)'

如果 running=1 且 8 卡 AICore 持续较高,通常不是假死。真正需要关注的是:日志长时间没有 step 进展、NPU 利用率降为 0、容器重启、出现 OOM,或者网络发送队列持续堆积。

若需要真正同时生成多个视频,最直接的办法是增加另一组独立的 8 卡副本;同一组 8 卡已经被单任务饱和。降低分辨率、时长或 steps 可以缩短单任务耗时,但会带来质量取舍。

8. 首尾帧 FL2VA 请求示例

MiniMax-H3 的首尾帧模式通过两个同名 input_references 字段按顺序上传,并设置 frame_indices=[0,-1]

curl -sS -X POST "${BASE_URL}/v1/videos" \
  -F 'prompt=The subject moves naturally from the first image to the last image.' \
  -F 'fps=24' \
  -F 'num_inference_steps=50' \
  -F 'flow_shift=12' \
  -F 'seed=2102' \
  -F 'extra_params={"task":"fl2va","duration":8.7,"frame_indices":[0,-1],"audio_flow_shift":3.0}' \
  -F 'input_references=@first_frame.png;type=image/png' \
  -F 'input_references=@last_frame.png;type=image/png'

提交后同样使用返回的 video_id 轮询和下载。

9. 验证清单

服务健康

curl -f http://127.0.0.1:9098/health
docker inspect minimax-h3-fl2va \
  --format 'status={{.State.Status}} restart={{.RestartCount}} oom={{.State.OOMKilled}}'

NPU 状态

npu-smi info

输出文件

ffprobe -v error \
  -show_entries stream=codec_name,width,height,r_frame_rate \
  -show_entries format=duration,size \
  -of json output.mp4

至少确认:

  • 视频编码为 H.264;
  • 分辨率和帧率正确;
  • AAC 音轨存在;
  • 时长与请求接近;
  • 容器没有重启或 OOM;
  • 输出文件已写入宿主机持久化目录。

总结

这次部署中最容易混淆的两件事是:

  1. 8 卡总显存充足,不代表单 rank 的 44.78 GiB 连续临时分配能够成功,packed-varlen 的价值是从根源上消除二次方 mask;
  2. 服务端推理完成,不代表同步长连接一定能把 MP4 成功送达客户端,长任务应优先使用异步任务 ID、轮询和独立下载。

完成 packed-varlen 修复、DLO、RainFusion 与异步任务改造后,MiniMax-H3 可以在 8×910B3 的 Atlas 800I A2 上稳定完成 15 秒 768P 视频+音频生成。后续优化重点可以放在真实 step 进度上报、任务元数据持久化,以及多副本并发调度。

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐