test/export_offline_bundle.sh

563 lines
18 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="${SCRIPT_DIR}"
TIMESTAMP="$(date +"%Y%m%d_%H%M%S")"
BUILD_TYPE=""
VERSION=""
OUTPUT_ROOT="${PROJECT_ROOT}/build-file"
REGISTRY="unis"
IMAGE_NAME="qwen3-asr"
INCLUDE_MODELS="true"
METAX_BASE_IMAGE="${METAX_BASE_IMAGE:-}"
ILUVATAR_BASE_IMAGE="${ILUVATAR_BASE_IMAGE:-}"
MTHREADS_BASE_IMAGE="${MTHREADS_BASE_IMAGE:-}"
METAX_PYTHON_BIN="${METAX_PYTHON_BIN:-/opt/conda/bin/python}"
ILUVATAR_PYTHON_BIN="${ILUVATAR_PYTHON_BIN:-python3}"
MTHREADS_PYTHON_BIN="${MTHREADS_PYTHON_BIN:-python3}"
info() { echo "[INFO] $1"; }
die() { echo "[ERROR] $1" >&2; exit 1; }
show_help() {
cat <<EOF
用法:
./export_offline_bundle.sh --type cpu|gpu|metax|iluvatar|mthreads|all [options]
选项:
-t, --type TYPE 构建类型: cpu、gpu、metax、iluvatar、mthreads 或 all
-v, --version VER 构建版本; 默认使用当前时间戳
-o, --output-root DIR 输出根目录; 默认: ${OUTPUT_ROOT}
-r, --registry REG 镜像仓库命名空间; 默认: ${REGISTRY}
--skip-models 不在离线交付目录中打包模型
--metax-base IMAGE 沐曦官方 vLLM 基础镜像(--type metax 时必填)
--iluvatar-base IMAGE 天数官方 vLLM 基础镜像(--type iluvatar 时必填)
--mthreads-base IMAGE 摩尔线程官方 vLLM 基础镜像(--type mthreads 时必填)
--metax-python BIN 沐曦镜像内 Python 路径; 默认: ${METAX_PYTHON_BIN}
--iluvatar-python BIN 天数镜像内 Python 路径; 默认: ${ILUVATAR_PYTHON_BIN}
--mthreads-python BIN 摩尔线程镜像内 Python 路径; 默认: ${MTHREADS_PYTHON_BIN}
-h, --help 显示帮助
示例:
./export_offline_bundle.sh --type gpu
./export_offline_bundle.sh --type metax
./export_offline_bundle.sh --type metax --metax-base cr.metax-tech.com/public-ai-release/maca/vllm-metax:0.17.0-maca.ai3.5.3.307-torch2.8-py312-ubuntu22.04-amd64 --skip-models
./export_offline_bundle.sh --type iluvatar --iluvatar-base registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v5
./export_offline_bundle.sh --type mthreads --mthreads-base registry.mthreads.com/presale/devtech/vllm_musa:s4000_4.3.5_d0519
./export_offline_bundle.sh --type cpu --version 1.0.1
./export_offline_bundle.sh --type all
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--type) BUILD_TYPE="$2"; shift 2 ;;
-v|--version) VERSION="$2"; shift 2 ;;
-o|--output-root) OUTPUT_ROOT="$2"; shift 2 ;;
-r|--registry) REGISTRY="$2"; shift 2 ;;
--metax-base) METAX_BASE_IMAGE="$2"; shift 2 ;;
--iluvatar-base) ILUVATAR_BASE_IMAGE="$2"; shift 2 ;;
--mthreads-base) MTHREADS_BASE_IMAGE="$2"; shift 2 ;;
--metax-python) METAX_PYTHON_BIN="$2"; shift 2 ;;
--iluvatar-python) ILUVATAR_PYTHON_BIN="$2"; shift 2 ;;
--mthreads-python) MTHREADS_PYTHON_BIN="$2"; shift 2 ;;
--skip-models) INCLUDE_MODELS="false"; shift ;;
-h|--help) show_help; exit 0 ;;
*) die "未知参数: $1" ;;
esac
done
}
prepare_offline_models() {
local bundle_dir="$1"
local model_export_dir="${bundle_dir}/models"
local model_archive="${bundle_dir}/qwen3-asr-models-${VERSION}.tar.gz"
if [[ "$INCLUDE_MODELS" != "true" ]]; then
info "跳过模型打包 (--skip-models)"
return 0
fi
info "下载并导出全部运行所需模型到离线交付目录"
rm -rf "$model_export_dir"
(
cd "$PROJECT_ROOT"
if command -v uv >/dev/null 2>&1; then
uv run python -m app.utils.download_models --export-dir "$model_export_dir"
else
python -m app.utils.download_models --export-dir "$model_export_dir"
fi
)
info "压缩模型目录: $(basename "$model_archive")"
tar -C "$bundle_dir" -czf "$model_archive" models
rm -rf "$model_export_dir"
}
prompt_build_type() {
local choice
echo "请选择离线交付类型:"
echo " 1) GPU"
echo " 2) CPU"
echo " 3) MetaX GPU"
echo " 4) Iluvatar GPU"
echo " 5) Moore Threads GPU"
echo " 6) ALL"
read -r -p "请输入选项 [6]: " choice
choice="${choice:-6}"
case "$choice" in
1) BUILD_TYPE="gpu" ;;
2) BUILD_TYPE="cpu" ;;
3) BUILD_TYPE="metax" ;;
4) BUILD_TYPE="iluvatar" ;;
5) BUILD_TYPE="mthreads" ;;
6) BUILD_TYPE="all" ;;
*) die "无效选项: $choice" ;;
esac
}
validate() {
case "$BUILD_TYPE" in
cpu|gpu|metax|iluvatar|mthreads|all) ;;
"") if [[ -t 0 ]]; then prompt_build_type; else BUILD_TYPE="all"; fi ;;
*) die "不支持的构建类型: ${BUILD_TYPE}" ;;
esac
if [[ "$BUILD_TYPE" == "metax" && -z "$METAX_BASE_IMAGE" ]]; then
die "--type metax 需要指定 --metax-base,值为已 docker load/pull 的沐曦官方 vLLM 镜像"
fi
if [[ "$BUILD_TYPE" == "iluvatar" && -z "$ILUVATAR_BASE_IMAGE" ]]; then
die "--type iluvatar 需要指定 --iluvatar-base,值为已 docker load/pull 的天数官方 vLLM 镜像"
fi
if [[ "$BUILD_TYPE" == "mthreads" && -z "$MTHREADS_BASE_IMAGE" ]]; then
die "--type mthreads 需要指定 --mthreads-base,值为已 docker load/pull 的摩尔线程官方 vLLM 镜像"
fi
VERSION="${VERSION:-$TIMESTAMP}"
}
export_compressor() {
command -v pigz >/dev/null 2>&1 && echo "pigz -f" || echo "gzip -f"
}
build_and_export_image() {
local target="$1"
local dockerfile="$2"
local tag="$3"
local bundle_dir="$4"
local tar_path="${bundle_dir}/${IMAGE_NAME}-${target}-${VERSION}-amd64.tar"
info "构建 ${target} 镜像: ${tag}"
(
cd "$PROJECT_ROOT"
case "$target" in
metax)
docker build -f "$dockerfile" -t "$tag" \
--build-arg "METAX_BASE_IMAGE=${METAX_BASE_IMAGE}" \
--build-arg "PYTHON_BIN=${METAX_PYTHON_BIN}" .
;;
iluvatar)
docker build -f "$dockerfile" -t "$tag" \
--build-arg "ILUVATAR_BASE_IMAGE=${ILUVATAR_BASE_IMAGE}" \
--build-arg "PYTHON_BIN=${ILUVATAR_PYTHON_BIN}" .
;;
mthreads)
docker build -f "$dockerfile" -t "$tag" \
--build-arg "MTHREADS_BASE_IMAGE=${MTHREADS_BASE_IMAGE}" \
--build-arg "PYTHON_BIN=${MTHREADS_PYTHON_BIN}" .
;;
*)
docker build -f "$dockerfile" -t "$tag" .
;;
esac
)
info "导出 ${target} 镜像归档"
docker save -o "$tar_path" "$tag"
info "压缩 ${target} 镜像归档"
$(export_compressor) "$tar_path"
}
build_offline_images() {
local bundle_dir="$1"
local cpu_tag="${REGISTRY}/${IMAGE_NAME}:cpu-${VERSION}"
local gpu_tag="${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}"
local metax_tag="${REGISTRY}/${IMAGE_NAME}:metax-${VERSION}"
local iluvatar_tag="${REGISTRY}/${IMAGE_NAME}:iluvatar-${VERSION}"
local mthreads_tag="${REGISTRY}/${IMAGE_NAME}:mthreads-${VERSION}"
case "$BUILD_TYPE" in
cpu)
build_and_export_image "cpu" "Dockerfile.cpu" "$cpu_tag" "$bundle_dir"
;;
gpu)
build_and_export_image "gpu" "Dockerfile.gpu" "$gpu_tag" "$bundle_dir"
;;
metax)
build_and_export_image "metax" "Dockerfile.metax" "$metax_tag" "$bundle_dir"
;;
iluvatar)
build_and_export_image "iluvatar" "Dockerfile.iluvatar" "$iluvatar_tag" "$bundle_dir"
;;
mthreads)
build_and_export_image "mthreads" "Dockerfile.mthreads" "$mthreads_tag" "$bundle_dir"
;;
all)
build_and_export_image "cpu" "Dockerfile.cpu" "$cpu_tag" "$bundle_dir"
build_and_export_image "gpu" "Dockerfile.gpu" "$gpu_tag" "$bundle_dir"
;;
esac
}
append_bundle_image_env() {
local bundle_dir="$1"
local env_file="${bundle_dir}/.env.example"
local image_tag=""
local note=""
case "$BUILD_TYPE" in
gpu)
image_tag="${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}"
note="GPU"
;;
metax)
image_tag="${REGISTRY}/${IMAGE_NAME}:metax-${VERSION}"
note="MetaX GPU"
;;
iluvatar)
image_tag="${REGISTRY}/${IMAGE_NAME}:iluvatar-${VERSION}"
note="Iluvatar GPU"
;;
mthreads)
image_tag="${REGISTRY}/${IMAGE_NAME}:mthreads-${VERSION}"
note="Moore Threads GPU"
;;
cpu)
image_tag="${REGISTRY}/${IMAGE_NAME}:cpu-${VERSION}"
note="CPU"
;;
all)
image_tag="${REGISTRY}/${IMAGE_NAME}:gpu-${VERSION}"
note="GPU by default; switch to ${REGISTRY}/${IMAGE_NAME}:cpu-${VERSION} when using docker-compose-cpu.yml"
;;
esac
cat >> "$env_file" <<EOF
# -----------------------------------------------------------------------------
# Offline bundle image tag (${note}).
# Keep this value aligned with the image loaded by docker load.
# -----------------------------------------------------------------------------
ASR_IMAGE=${image_tag}
EOF
}
create_bundle_env() {
local bundle_dir="$1"
cp "${bundle_dir}/.env.example" "${bundle_dir}/.env"
}
bundle_compose_files() {
case "$BUILD_TYPE" in
gpu)
printf '%s\n' "docker-compose.yml"
;;
metax)
printf '%s\n' "docker-compose-metax.yml"
;;
iluvatar)
printf '%s\n' "docker-compose-iluvatar.yml"
;;
mthreads)
printf '%s\n' "docker-compose-mthreads.yml"
;;
cpu)
printf '%s\n' "docker-compose-cpu.yml"
;;
all)
printf '%s\n' "docker-compose.yml" "docker-compose-cpu.yml"
;;
esac
}
compose_description() {
case "$1" in
docker-compose.yml) echo "NVIDIA GPU 版 compose 文件" ;;
docker-compose-cpu.yml) echo "CPU 版 compose 文件" ;;
docker-compose-metax.yml) echo "沐曦 GPU 版 compose 文件" ;;
docker-compose-iluvatar.yml) echo "天数 GPU 版 compose 文件" ;;
docker-compose-mthreads.yml) echo "摩尔线程 GPU 版 compose 文件" ;;
*) echo "compose 文件" ;;
esac
}
copy_bundle_files() {
local bundle_dir="$1"
local compose_file
while IFS= read -r compose_file; do
[[ -n "$compose_file" ]] || continue
cp "${PROJECT_ROOT}/${compose_file}" "${bundle_dir}/${compose_file}"
done < <(bundle_compose_files)
cp "${PROJECT_ROOT}/.env.example" "${bundle_dir}/.env.example"
cp "${PROJECT_ROOT}/docs/deployment.md" "${bundle_dir}/DEPLOYMENT.md"
if [[ "$BUILD_TYPE" == "metax" ]]; then
cp "${PROJECT_ROOT}/docs/metax_offline_deployment.md" "${bundle_dir}/METAX_DEPLOYMENT.md"
fi
if [[ "$BUILD_TYPE" == "iluvatar" ]]; then
cp "${PROJECT_ROOT}/docs/iluvatar_offline_deployment.md" "${bundle_dir}/ILUVATAR_DEPLOYMENT.md"
fi
if [[ "$BUILD_TYPE" == "mthreads" ]]; then
cp "${PROJECT_ROOT}/docs/mthreads_offline_deployment.md" "${bundle_dir}/MTHREADS_DEPLOYMENT.md"
fi
cp "${PROJECT_ROOT}/scripts/docker/init_host_dirs.sh" "${bundle_dir}/init_host_dirs.sh"
cp "${PROJECT_ROOT}/scripts/download-models.sh" "${bundle_dir}/download-models.sh"
cp "${PROJECT_ROOT}/scripts/download_models_standalone.py" "${bundle_dir}/download_models_standalone.py"
chmod +x "${bundle_dir}/init_host_dirs.sh"
chmod +x "${bundle_dir}/download-models.sh"
append_bundle_image_env "$bundle_dir"
create_bundle_env "$bundle_dir"
}
generate_bundle_metadata() {
local bundle_dir="$1"
shift
local image_archives=("$@")
local archive
local compose_file
cat > "${bundle_dir}/BUNDLE_INFO.txt" <<EOF
Qwen3-ASR Offline Bundle
========================
Bundle Type : ${BUILD_TYPE}
Version : ${VERSION}
Timestamp : ${TIMESTAMP}
Image Files :
EOF
for archive in "${image_archives[@]}"; do
echo " ${archive}" >> "${bundle_dir}/BUNDLE_INFO.txt"
done
echo "Compose Files:" >> "${bundle_dir}/BUNDLE_INFO.txt"
while IFS= read -r compose_file; do
[[ -n "$compose_file" ]] || continue
echo " ${compose_file}" >> "${bundle_dir}/BUNDLE_INFO.txt"
done < <(bundle_compose_files)
cat >> "${bundle_dir}/BUNDLE_INFO.txt" <<EOF
Host Paths :
/opt/dep/asr/models
/opt/dep/asr/data
EOF
if [[ "$INCLUDE_MODELS" == "true" ]]; then
cat >> "${bundle_dir}/BUNDLE_INFO.txt" <<EOF
Model Files :
qwen3-asr-models-${VERSION}.tar.gz
EOF
fi
}
generate_bundle_readme() {
local bundle_dir="$1"
shift
local image_archives=("$@")
local content_lines=""
local import_lines=""
local startup_lines=""
local hint_lines=""
local status_lines=""
local compose_lines=""
local archive
local compose_file
for archive in "${image_archives[@]}"; do
content_lines="${content_lines}- \`${archive}\`: Docker 镜像归档"$'\n'
import_lines="${import_lines}gunzip -c ${archive} | docker load"$'\n'
done
if [[ "$BUILD_TYPE" == "gpu" ]]; then
startup_lines=$'docker compose up -d'
status_lines=$'docker compose ps\ndocker compose logs -f'
hint_lines="- GPU 版默认按机器资源自动选择 Qwen3-ASR 模型。"
elif [[ "$BUILD_TYPE" == "metax" ]]; then
startup_lines=$'docker compose -f docker-compose-metax.yml up -d'
status_lines=$'docker compose -f docker-compose-metax.yml ps\ndocker compose -f docker-compose-metax.yml logs -f'
hint_lines="- 沐曦 GPU 版使用 docker-compose-metax.yml,并要求目标机已安装沐曦驱动/容器运行栈。"
elif [[ "$BUILD_TYPE" == "iluvatar" ]]; then
startup_lines=$'docker compose -f docker-compose-iluvatar.yml up -d'
status_lines=$'docker compose -f docker-compose-iluvatar.yml ps\ndocker compose -f docker-compose-iluvatar.yml logs -f'
hint_lines="- 天数 GPU 版使用 docker-compose-iluvatar.yml,并沿用官方镜像建议的 host network、host pid/ipc、privileged 与设备挂载。"
elif [[ "$BUILD_TYPE" == "mthreads" ]]; then
startup_lines=$'docker compose -f docker-compose-mthreads.yml up -d'
status_lines=$'docker compose -f docker-compose-mthreads.yml ps\ndocker compose -f docker-compose-mthreads.yml logs -f'
hint_lines="- 摩尔线程 GPU 版使用 docker-compose-mthreads.yml,并沿用官方 MUSA vLLM 镜像建议的 host network、host pid/ipc、privileged 与设备挂载。"
elif [[ "$BUILD_TYPE" == "cpu" ]]; then
startup_lines=$'docker compose -f docker-compose-cpu.yml up -d'
status_lines=$'docker compose -f docker-compose-cpu.yml ps\ndocker compose -f docker-compose-cpu.yml logs -f'
hint_lines="- CPU 版默认走 vendored QwenASR Rust backend,通常会使用 qwen3-asr-0.6b。"
else
startup_lines=$'docker compose up -d\n# 或仅启动 CPU 版本\n# docker compose -f docker-compose-cpu.yml up -d'
status_lines=$'docker compose ps\ndocker compose logs -f'
hint_lines=$'- ALL 模式会同时打包 GPU 与 CPU 镜像,目标机可按需选择加载和启动。\n- GPU 版默认按机器资源自动选择 Qwen3-ASR 模型。\n- CPU 版默认走 vendored QwenASR Rust backend,通常会使用 qwen3-asr-0.6b。'
fi
while IFS= read -r compose_file; do
[[ -n "$compose_file" ]] || continue
compose_lines="${compose_lines}- \`${compose_file}\`: $(compose_description "$compose_file")"$'\n'
done < <(bundle_compose_files)
cat > "${bundle_dir}/README.md" <<EOF
# Qwen3-ASR 离线交付目录
这是一个可直接拷贝到目标机的离线交付目录。
## 内容说明
${compose_lines}
- \`.env.example\`: 环境变量示例
- \`init_host_dirs.sh\`: 初始化宿主机挂载目录
- \`download-models.sh\`: 增量下载缺失模型,不删除现有模型目录
- \`DEPLOYMENT.md\`: 详细部署文档
$(if [[ "$BUILD_TYPE" == "metax" ]]; then echo "- \`METAX_DEPLOYMENT.md\`: 沐曦 GPU 国产化离线部署文档"; fi)
$(if [[ "$BUILD_TYPE" == "iluvatar" ]]; then echo "- \`ILUVATAR_DEPLOYMENT.md\`: 天数 GPU 国产化离线部署文档"; fi)
$(if [[ "$BUILD_TYPE" == "mthreads" ]]; then echo "- \`MTHREADS_DEPLOYMENT.md\`: 摩尔线程 GPU 国产化离线部署文档"; fi)
- \`BUNDLE_INFO.txt\`: 本次交付元信息
${content_lines}
$(if [[ "$INCLUDE_MODELS" == "true" ]]; then echo "- \`qwen3-asr-models-${VERSION}.tar.gz\`: 全量离线模型包"; fi)
## 本次交付
- 类型: \`${BUILD_TYPE}\`
- 版本: \`${VERSION}\`
- 时间: \`${TIMESTAMP}\`
## 使用步骤
1. 把整个目录复制到目标机,例如 \`/opt/dep/asr/bundles/${TIMESTAMP}-${BUILD_TYPE}\`
2. 进入目录并初始化宿主机挂载目录:
\`\`\`bash
chmod +x init_host_dirs.sh
./init_host_dirs.sh
\`\`\`
3. 准备模型目录内容
$(if [[ "$INCLUDE_MODELS" == "true" ]]; then cat <<MODEL_EOF
\`\`\`bash
tar -xzf qwen3-asr-models-${VERSION}.tar.gz -C /opt/dep/asr/
\`\`\`
模型会解压到 \`/opt/dep/asr/models\`,容器内默认挂载为 \`/app/models\`。
MODEL_EOF
else cat <<MODEL_EOF
- 本次打包使用了 \`--skip-models\`,需要另外准备模型目录
- 如果目标机可联网,可运行 \`./download-models.sh --models-dir /opt/dep/asr/models\` 增量补齐模型
- 如果目标机不能联网,请在联网机器上准备模型目录或模型包,再复制到 \`/opt/dep/asr/models\`
MODEL_EOF
fi)
4. 导入镜像
\`\`\`bash
${import_lines}\`\`\`
5. 准备配置
- 离线包已自动生成 \`.env\`,里面包含本次镜像对应的 \`ASR_IMAGE\`,不要改回 \`latest\`
- 按需修改 \`.env\` 里的 \`API_KEY\`、\`CUDA_VISIBLE_DEVICES\` 等变量
- 默认宿主机挂载目录:
- \`/opt/dep/asr/models\`
- \`/opt/dep/asr/data\`(包含 logs、temp、tasks)
6. 启动服务
\`\`\`bash
${startup_lines}
\`\`\`
7. 查看状态
\`\`\`bash
${status_lines}
\`\`\`
## 说明
${hint_lines}
- 如果目标机不能联网,请使用本目录内的模型包;不要依赖运行时下载
- 更完整的说明见 \`DEPLOYMENT.md\`
EOF
}
collect_image_archives() {
local bundle_dir="$1"
local archives=()
case "$BUILD_TYPE" in
gpu)
archives+=("qwen3-asr-gpu-${VERSION}-amd64.tar.gz")
;;
metax)
archives+=("qwen3-asr-metax-${VERSION}-amd64.tar.gz")
;;
iluvatar)
archives+=("qwen3-asr-iluvatar-${VERSION}-amd64.tar.gz")
;;
mthreads)
archives+=("qwen3-asr-mthreads-${VERSION}-amd64.tar.gz")
;;
cpu)
archives+=("qwen3-asr-cpu-${VERSION}-amd64.tar.gz")
;;
all)
archives+=(
"qwen3-asr-cpu-${VERSION}-amd64.tar.gz"
"qwen3-asr-gpu-${VERSION}-amd64.tar.gz"
)
;;
esac
local archive
for archive in "${archives[@]}"; do
[[ -f "${bundle_dir}/${archive}" ]] || die "未找到导出的镜像压缩包: ${archive}"
done
printf '%s\n' "${archives[@]}"
}
main() {
parse_args "$@"
validate
mkdir -p "$OUTPUT_ROOT"
local bundle_dir="${OUTPUT_ROOT}/${TIMESTAMP}-${BUILD_TYPE}"
mkdir -p "$bundle_dir"
info "输出目录: ${bundle_dir}"
info "开始构建 ${BUILD_TYPE} 离线交付包(普通 docker build/save)"
build_offline_images "$bundle_dir"
prepare_offline_models "$bundle_dir"
mapfile -t image_archives < <(collect_image_archives "$bundle_dir")
copy_bundle_files "$bundle_dir"
generate_bundle_metadata "$bundle_dir" "${image_archives[@]}"
generate_bundle_readme "$bundle_dir" "${image_archives[@]}"
info "离线交付目录已生成"
info "目录: ${bundle_dir}"
local archive
for archive in "${image_archives[@]}"; do
info "镜像: ${archive}"
done
}
main "$@"