Scope engine image builds and verification by release target
All checks were successful
Build and verify Qwen2.5 compatibility engines / docker (push) Successful in 1m59s
All checks were successful
Build and verify Qwen2.5 compatibility engines / docker (push) Successful in 1m59s
This commit is contained in:
@@ -3,23 +3,33 @@ name: Build and verify Qwen2.5 compatibility engines
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "v*-nvidia-a100"
|
||||
- "v*-cambricon-mlu370"
|
||||
- "v*-metax-c500"
|
||||
- "v*-vastai-va16"
|
||||
- "v*-all"
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: amd64-ubuntu-24.04
|
||||
env:
|
||||
RELEASE_TAG: ${{ gitea.ref_name }}
|
||||
REPOSITORY: ${{ gitea.repository }}
|
||||
SERVER_URL: ${{ gitea.server_url }}
|
||||
ACTOR: ${{ gitea.actor }}
|
||||
steps:
|
||||
- name: Clone tagged source
|
||||
run: |
|
||||
git clone "${{ gitea.server_url }}/${{ gitea.repository }}.git" .
|
||||
git checkout "${{ gitea.ref_name }}"
|
||||
git clone "$SERVER_URL/$REPOSITORY.git" .
|
||||
git checkout "$RELEASE_TAG"
|
||||
|
||||
- name: Run compatibility tests
|
||||
run: |
|
||||
python3 -m unittest discover -s tests -q
|
||||
python3 -m py_compile engine_compat.py release_plan.py
|
||||
bash -n vllm_wrapper.sh
|
||||
|
||||
- name: Load image metadata
|
||||
- name: Select release targets and load image metadata
|
||||
run: |
|
||||
set -eu
|
||||
set -a
|
||||
@@ -27,10 +37,9 @@ jobs:
|
||||
set +a
|
||||
test -n "$FRAMEWORK"
|
||||
test -n "$TASK_TYPE"
|
||||
test -n "$GPU_TYPES"
|
||||
python3 release_plan.py "$RELEASE_TAG" --gitea-env "$GITEA_ENV"
|
||||
echo "FRAMEWORK=$FRAMEWORK" >> "$GITEA_ENV"
|
||||
echo "TASK_TYPE=$TASK_TYPE" >> "$GITEA_ENV"
|
||||
echo "GPU_TYPES=$GPU_TYPES" >> "$GITEA_ENV"
|
||||
|
||||
- name: Validate GPU and task metadata
|
||||
run: |
|
||||
@@ -58,29 +67,37 @@ jobs:
|
||||
- name: Build and push target images
|
||||
run: |
|
||||
set -eu
|
||||
image_name="$(printf '%s' '${{ gitea.repository }}' | tr '[:upper:]' '[:lower:]' | tr '_' '-')"
|
||||
image_name="$(printf '%s' "$REPOSITORY" | tr '[:upper:]' '[:lower:]' | tr '_' '-')"
|
||||
image_base="$DOCKER_REGISTRY/$DOCKER_USERNAME/$image_name"
|
||||
a100_image="$image_base:${{ gitea.ref_name }}-nvidia-a100"
|
||||
cambricon_image="$image_base:${{ gitea.ref_name }}-cambricon-mlu370"
|
||||
metax_image="$image_base:${{ gitea.ref_name }}-metax-c500"
|
||||
vastai_image="$image_base:${{ gitea.ref_name }}-vastai-va16"
|
||||
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/dooke/vllm/vllm/vllm-openai:v0.11.0 \
|
||||
-t "$a100_image" .
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/hardcore-tech/cambricon-mlu370-pytorch:v25.01-20260228 \
|
||||
-t "$cambricon_image" .
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/modelhubxc/enginex-metax/vllm:0.9.1 \
|
||||
--build-arg PYTHON_BIN=/opt/conda/bin/python3 \
|
||||
--build-arg VLLM_BIN=/opt/conda/bin/vllm \
|
||||
-t "$metax_image" .
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/modelhubxc/sunruoxi/enginex-vastai-vllm-vacc-fix-tokenizer:VVI-26.02 \
|
||||
-t "$vastai_image" .
|
||||
|
||||
for image in "$a100_image" "$cambricon_image" "$metax_image" "$vastai_image"; do
|
||||
echo "IMAGE_BASE=$image_base" >> "$GITEA_ENV"
|
||||
for target in $IMAGE_TARGETS; do
|
||||
if [ "$RELEASE_TARGET" = all ]; then
|
||||
image_tag="$RELEASE_TAG-$target"
|
||||
else
|
||||
image_tag="$RELEASE_TAG"
|
||||
fi
|
||||
image="$image_base:$image_tag"
|
||||
case "$target" in
|
||||
nvidia-a100)
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/dooke/vllm/vllm/vllm-openai:v0.11.0 \
|
||||
-t "$image" . ;;
|
||||
cambricon-mlu370)
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/hardcore-tech/cambricon-mlu370-pytorch:v25.01-20260228 \
|
||||
-t "$image" . ;;
|
||||
metax-c500)
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/modelhubxc/enginex-metax/vllm:0.9.1 \
|
||||
--build-arg PYTHON_BIN=/opt/conda/bin/python3 \
|
||||
--build-arg VLLM_BIN=/opt/conda/bin/vllm \
|
||||
-t "$image" . ;;
|
||||
vastai-va16)
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=harbor.4pd.io/modelhubxc/sunruoxi/enginex-vastai-vllm-vacc-fix-tokenizer:VVI-26.02 \
|
||||
-t "$image" . ;;
|
||||
*) echo "unknown image target: $target"; exit 1 ;;
|
||||
esac
|
||||
success=0
|
||||
for attempt in 1 2 3; do
|
||||
if docker push "$image"; then success=1; break; fi
|
||||
@@ -89,23 +106,24 @@ jobs:
|
||||
done
|
||||
test "$success" -eq 1
|
||||
done
|
||||
echo "A100_IMAGE=$a100_image" >> "$GITEA_ENV"
|
||||
echo "CAMBRICON_IMAGE=$cambricon_image" >> "$GITEA_ENV"
|
||||
echo "METAX_IMAGE=$metax_image" >> "$GITEA_ENV"
|
||||
echo "VASTAI_IMAGE=$vastai_image" >> "$GITEA_ENV"
|
||||
|
||||
- name: Notify ModelHub image verification
|
||||
run: |
|
||||
set -eu
|
||||
for gpu in $GPU_TYPES; do
|
||||
case "$gpu" in
|
||||
Nvidia-A100) image="$A100_IMAGE" ;;
|
||||
Cambricon_mlu-370-x4|Cambricon_mlu-370-x8) image="$CAMBRICON_IMAGE" ;;
|
||||
MetaX_c-500) image="$METAX_IMAGE" ;;
|
||||
Vastai_va16) image="$VASTAI_IMAGE" ;;
|
||||
*) echo "unknown GPU target: $gpu"; exit 1 ;;
|
||||
for mapping in $VERIFY_PAIRS; do
|
||||
gpu="${mapping%%:*}"
|
||||
target="${mapping#*:}"
|
||||
case " $IMAGE_TARGETS " in
|
||||
*" $target "*) ;;
|
||||
*) echo "GPU target was not built: $gpu"; exit 1 ;;
|
||||
esac
|
||||
payload="$(GPU_TYPE="$gpu" IMAGE="$image" REPO_URL='${{ gitea.server_url }}/${{ gitea.repository }}' TAG='${{ gitea.ref_name }}' ACTOR='${{ gitea.actor }}' python3 -c 'import json, os; print(json.dumps({"framework": os.environ["FRAMEWORK"], "gpuType": os.environ["GPU_TYPE"], "imageUrl": os.environ["IMAGE"], "taskType": os.environ["TASK_TYPE"], "createBy": os.environ["ACTOR"], "repoUrl": os.environ["REPO_URL"], "tag": os.environ["TAG"]}))')"
|
||||
if [ "$RELEASE_TARGET" = all ]; then
|
||||
image_tag="$RELEASE_TAG-$target"
|
||||
else
|
||||
image_tag="$RELEASE_TAG"
|
||||
fi
|
||||
image="$IMAGE_BASE:$image_tag"
|
||||
payload="$(GPU_TYPE="$gpu" IMAGE="$image" REPO_URL="$SERVER_URL/$REPOSITORY" TAG="$RELEASE_TAG" python3 -c 'import json, os; print(json.dumps({"framework": os.environ["FRAMEWORK"], "gpuType": os.environ["GPU_TYPE"], "imageUrl": os.environ["IMAGE"], "taskType": os.environ["TASK_TYPE"], "createBy": os.environ["ACTOR"], "repoUrl": os.environ["REPO_URL"], "tag": os.environ["TAG"]}))')"
|
||||
response="$(curl --silent --show-error --fail-with-body --location \
|
||||
--request POST 'https://modelhub.org.cn/adminApi/image-verify' \
|
||||
--header "Xc-Token: $FIXED_TOKEN" \
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
FRAMEWORK=vllm_qwen25_compat
|
||||
TASK_TYPE=text-generation
|
||||
GPU_TYPES="Nvidia-A100 Cambricon_mlu-370-x4 Cambricon_mlu-370-x8 MetaX_c-500 Vastai_va16"
|
||||
|
||||
24
README.md
24
README.md
@@ -4,8 +4,9 @@
|
||||
的方式:标签触发 `.gitea/workflows/docker-build-push.yml`,runner 检查元数据、构建并推送镜像,
|
||||
最后调用 ModelHub 的 `image-verify` 接口注册验证。
|
||||
|
||||
当前为 A100 参考端、寒武纪 MLU370 x4/x8、沐曦 C500 和 Vastai VA16
|
||||
构建四个镜像,五个 GPU 共用 `vllm_qwen25_compat` 框架名。基础镜像均来自
|
||||
支持 A100 参考端、寒武纪 MLU370 x4/x8、沐曦 C500 和 Vastai VA16;
|
||||
一次显式全量发布构建四个镜像,五个 GPU 共用 `vllm_qwen25_compat` 框架名。
|
||||
寒武纪 x4/x8 共用一个镜像,但仍分别进行平台验证。基础镜像均来自
|
||||
平台已有验证任务使用的版本;A100 沿用示例仓库的基础镜像。每个 GPU 的注册
|
||||
与可用性仍以平台镜像验证结果和 `frameworks` 接口回读为准。
|
||||
|
||||
@@ -52,14 +53,21 @@ docker build --build-arg BASE_IMAGE=harbor.4pd.io/modelhubxc/sunruoxi/enginex-va
|
||||
|
||||
## 用 runner 发布
|
||||
|
||||
1. 源码在公开仓库 `a4913994/enginex-qwen25-compat` 的 `main`;`v0.1.0`
|
||||
已完成 A100/寒武纪镜像构建并提交镜像验证,验证结果须单独回读。
|
||||
1. 源码在公开仓库 `a4913994/enginex-qwen25-compat` 的 `main`。推送源码不会构建镜像;
|
||||
只有带目标后缀的版本标签才触发 runner。历史 `v0.1.0` 和 `v0.2.1` 的镜像验证
|
||||
结果仍须单独回读。
|
||||
2. runner 使用 `DOCKER_REGISTRY`、`DOCKER_USERNAME`、`DOCKER_PASSWORD`、
|
||||
`FIXED_TOKEN`;不要把凭证写进仓库。
|
||||
3. 推送新的 `v*` 标签后,工作流依次跑单元测试、验证 GPU/任务元数据、构建与推送
|
||||
镜像,并为五个 GPU 各发起一次镜像验证;接口业务码也会被检查。
|
||||
4. 在“我的驱动列表”确认五个镜像验证通过,并检查新框架出现在对应 GPU 的
|
||||
`frameworks` 接口中,再用少量已知缺陷模型做对照验证。
|
||||
3. 选择本次确实需要更新的镜像目标,推送 `vX.Y.Z-nvidia-a100`、
|
||||
`vX.Y.Z-cambricon-mlu370`、`vX.Y.Z-metax-c500` 或 `vX.Y.Z-vastai-va16`。
|
||||
例如 `v0.3.0-metax-c500` 只构建一份沐曦镜像并验证沐曦 C500;寒武纪标签
|
||||
构建一份镜像并分别验证 x4/x8。只有明确使用 `vX.Y.Z-all` 才构建四份镜像、
|
||||
发起五项验证。普通 `vX.Y.Z` 标签不触发工作流,也不会隐式全量发布。
|
||||
4. 工作流先跑单元测试和目标 GPU 元数据校验,再构建、推送选定镜像并检查平台
|
||||
`image-verify` 接口业务码。通过本目录的 `python release_plan.py v0.3.0-metax-c500`
|
||||
可以在打标签前预览将构建与验证的目标。
|
||||
5. 回读平台镜像验证记录,并检查新框架出现在目标 GPU 的 `frameworks` 接口中,
|
||||
再用少量已知缺陷模型做对照验证。`pending` 不代表镜像已经可用。
|
||||
|
||||
只有平台确认新框架可选且 A100 参考端实际使用新镜像后,才应让 `xc-agent` 把
|
||||
列表型 `extra_special_tokens` 的模型路由到此框架。当前智能体继续预检并跳过它们。
|
||||
|
||||
74
release_plan.py
Normal file
74
release_plan.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Select the image builds and GPU verifications for a scoped release tag."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
GPU_GROUPS = {
|
||||
"nvidia-a100": ("Nvidia-A100",),
|
||||
"cambricon-mlu370": ("Cambricon_mlu-370-x4", "Cambricon_mlu-370-x8"),
|
||||
"metax-c500": ("MetaX_c-500",),
|
||||
"vastai-va16": ("Vastai_va16",),
|
||||
}
|
||||
RELEASE_TAG = re.compile(
|
||||
r"v[0-9]+\.[0-9]+\.[0-9]+-(nvidia-a100|cambricon-mlu370|metax-c500|vastai-va16|all)\Z"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleasePlan:
|
||||
target: str
|
||||
image_targets: tuple[str, ...]
|
||||
gpu_types: tuple[str, ...]
|
||||
verification_pairs: tuple[tuple[str, str], ...]
|
||||
|
||||
|
||||
def plan_for_tag(tag: str) -> ReleasePlan:
|
||||
match = RELEASE_TAG.fullmatch(tag)
|
||||
if match is None:
|
||||
raise ValueError(
|
||||
"release tag must be vX.Y.Z-<target>, where target is "
|
||||
"nvidia-a100, cambricon-mlu370, metax-c500, vastai-va16, or all"
|
||||
)
|
||||
target = match.group(1)
|
||||
image_targets = tuple(GPU_GROUPS) if target == "all" else (target,)
|
||||
verification_pairs = tuple(
|
||||
(gpu, group) for group in image_targets for gpu in GPU_GROUPS[group]
|
||||
)
|
||||
gpu_types = tuple(gpu for gpu, _ in verification_pairs)
|
||||
return ReleasePlan(target, image_targets, gpu_types, verification_pairs)
|
||||
|
||||
|
||||
def write_gitea_env(plan: ReleasePlan, path: Path) -> None:
|
||||
with path.open("a", encoding="utf-8") as env_file:
|
||||
env_file.write(f"RELEASE_TARGET={plan.target}\n")
|
||||
env_file.write(f"IMAGE_TARGETS={' '.join(plan.image_targets)}\n")
|
||||
env_file.write(f"GPU_TYPES={' '.join(plan.gpu_types)}\n")
|
||||
env_file.write(
|
||||
"VERIFY_PAIRS="
|
||||
+ " ".join(f"{gpu}:{group}" for gpu, group in plan.verification_pairs)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("tag", help="scoped release tag, such as v0.3.0-metax-c500")
|
||||
parser.add_argument("--gitea-env", type=Path, help="append the plan to GITEA_ENV")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
plan = plan_for_tag(args.tag)
|
||||
except ValueError as exc:
|
||||
parser.error(str(exc))
|
||||
if args.gitea_env:
|
||||
write_gitea_env(plan, args.gitea_env)
|
||||
print(json.dumps(plan.__dict__, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
66
tests/test_release_plan.py
Normal file
66
tests/test_release_plan.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from release_plan import GPU_GROUPS, plan_for_tag, write_gitea_env
|
||||
|
||||
|
||||
class ReleasePlanTests(unittest.TestCase):
|
||||
def test_one_vendor_tag_builds_only_that_image_and_gpu(self):
|
||||
plan = plan_for_tag("v0.3.0-metax-c500")
|
||||
self.assertEqual(plan.target, "metax-c500")
|
||||
self.assertEqual(plan.image_targets, ("metax-c500",))
|
||||
self.assertEqual(plan.gpu_types, ("MetaX_c-500",))
|
||||
self.assertEqual(plan.verification_pairs, (("MetaX_c-500", "metax-c500"),))
|
||||
|
||||
def test_cambricon_cards_reuse_one_image(self):
|
||||
plan = plan_for_tag("v0.3.0-cambricon-mlu370")
|
||||
self.assertEqual(plan.image_targets, ("cambricon-mlu370",))
|
||||
self.assertEqual(
|
||||
plan.gpu_types,
|
||||
("Cambricon_mlu-370-x4", "Cambricon_mlu-370-x8"),
|
||||
)
|
||||
self.assertEqual(
|
||||
plan.verification_pairs,
|
||||
(
|
||||
("Cambricon_mlu-370-x4", "cambricon-mlu370"),
|
||||
("Cambricon_mlu-370-x8", "cambricon-mlu370"),
|
||||
),
|
||||
)
|
||||
|
||||
def test_all_requires_explicit_suffix(self):
|
||||
plan = plan_for_tag("v0.3.0-all")
|
||||
self.assertEqual(plan.image_targets, tuple(GPU_GROUPS))
|
||||
self.assertEqual(len(plan.gpu_types), 5)
|
||||
self.assertEqual(len(set(plan.gpu_types)), 5)
|
||||
|
||||
def test_unscoped_or_unknown_tag_cannot_default_to_all(self):
|
||||
for tag in (
|
||||
"v0.3.0",
|
||||
"v0.3.0-unknown",
|
||||
"v0.3.0-metax-c500-extra",
|
||||
"v0.3.0-all",
|
||||
"latest",
|
||||
):
|
||||
with self.subTest(tag=tag), self.assertRaises(ValueError):
|
||||
plan_for_tag(tag)
|
||||
|
||||
def test_gitea_env_contains_only_selected_targets(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "gitea_env"
|
||||
path.write_text("FRAMEWORK=vllm_qwen25_compat\n", encoding="utf-8")
|
||||
write_gitea_env(plan_for_tag("v0.3.0-vastai-va16"), path)
|
||||
self.assertEqual(
|
||||
path.read_text(encoding="utf-8").splitlines(),
|
||||
[
|
||||
"FRAMEWORK=vllm_qwen25_compat",
|
||||
"RELEASE_TARGET=vastai-va16",
|
||||
"IMAGE_TARGETS=vastai-va16",
|
||||
"GPU_TYPES=Vastai_va16",
|
||||
"VERIFY_PAIRS=Vastai_va16:vastai-va16",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user