Auto-detect Scout/Maverick from config.json and add verify script

This commit is contained in:
2026-10-26 16:59:44 +08:00
parent 13f14a12e9
commit 738d9f80e4
10 changed files with 816 additions and 235 deletions

View File

@@ -2,6 +2,6 @@
.gitea .gitea
README.md README.md
run.sh run.sh
verify.sh
docker-compose.yml docker-compose.yml
presets
*.log *.log

View File

@@ -4,14 +4,15 @@
# - Llama-4-Scout-17B-16E-Instruct(BF16,TP8,8 卡) # - Llama-4-Scout-17B-16E-Instruct(BF16,TP8,8 卡)
# - Llama-4-Maverick-17B-128E-Instruct-FP8(FP8,TP8+PP2,16 卡) # - Llama-4-Maverick-17B-128E-Instruct-FP8(FP8,TP8+PP2,16 卡)
# #
# 模型权重不打进镜像,运行时按 run.sh 的 scout / maverick 预设挂载。 # 模型权重不打进镜像,运行时挂载模型目录;容器启动后读取 config.json
# 自动判断 Scout / Maverick,并选择对应的 TP / PP / 端口。
# #
# 构建: # 构建:
# docker build -t llama4-bi150:2026-10-26 . # docker build -t llama4-bi150:2026-10-26 .
# #
# 运行: # 运行:
# ./run.sh scout # ./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct
# ./run.sh maverick # ./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
# #
# 补丁是针对下面这个精确基础镜像生成的;换基础镜像需要重新生成 diff。 # 补丁是针对下面这个精确基础镜像生成的;换基础镜像需要重新生成 diff。
# registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v7 # registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v7
@@ -27,17 +28,16 @@ LABEL org.opencontainers.image.title="llama4-bi150" \
COPY patches /opt/llama4/patches COPY patches /opt/llama4/patches
COPY apply_patch.py /opt/llama4/apply_patch.py COPY apply_patch.py /opt/llama4/apply_patch.py
COPY detect_model.py /opt/llama4/detect_model.py
COPY entrypoint.sh /usr/local/bin/llama4-entrypoint.sh COPY entrypoint.sh /usr/local/bin/llama4-entrypoint.sh
RUN chmod +x /usr/local/bin/llama4-entrypoint.sh \ RUN chmod +x /usr/local/bin/llama4-entrypoint.sh \
&& chmod +x /opt/llama4/detect_model.py \
&& python3 /opt/llama4/apply_patch.py && python3 /opt/llama4/apply_patch.py
ENV MODEL_PATH=/models/llama4 \ ENV MODEL_PATH=/models/llama4 \
SERVED_NAME=llama4 \ AUTO_DETECT=1 \
HOST=0.0.0.0 \ HOST=0.0.0.0 \
PORT=8091 \
TP=8 \
PP=1 \
MAX_MODEL_LEN=4096 \ MAX_MODEL_LEN=4096 \
MAX_NUM_SEQS=1 \ MAX_NUM_SEQS=1 \
GPU_MEM_UTIL=0.90 \ GPU_MEM_UTIL=0.90 \

242
README.md
View File

@@ -3,17 +3,17 @@
在 16×Iluvatar BI-V150 32GB 上运行 Llama 4 Scout 与 Maverick 的统一 Docker 部署仓库。 在 16×Iluvatar BI-V150 32GB 上运行 Llama 4 Scout 与 Maverick 的统一 Docker 部署仓库。
> 本仓库合并自原先独立的 `llama4-scout-deploy` 和 `llama4-maverick-deploy`, > 本仓库合并自原先独立的 `llama4-scout-deploy` 和 `llama4-maverick-deploy`,
> 用一个镜像 + 两个运行预设替代两套重复的 Dockerfile。 > 用一个镜像 + 自动模型检测替代两套重复的 Dockerfile 和手动 profile。
- 基础镜像:`registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v7`(vLLM `0.17.0+corex`) - 基础镜像:`registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v7`(vLLM `0.17.0+corex`)
- 一个镜像同时支持 Scout 和 Maverick;模型权重不打进镜像,运行时只读挂载 - 一个镜像同时支持 Scout 和 Maverick;模型权重不打进镜像,运行时只读挂载
- 两个模型共用 Llama 4 chunked-attention 补丁;Maverick 额外使用 FP8 量化 / FP8 MoE 补丁 - 两个模型共用 Llama 4 chunked-attention 补丁;Maverick 额外使用 FP8 量化 / FP8 MoE 补丁
- CI 使用 ModelHub 标准 Gitea Actions:推送 `v*` 标签后构建、推送镜像并提交审核 - CI 使用 ModelHub 标准 Gitea Actions:推送 `v*` 标签后构建、推送镜像并提交审核
| 模型 | 精度 | 并行 | GPU 数 | 默认端口 | | 模型 | 精度 | 自动选择 | GPU 数 | 默认端口 |
|---|---|---|---|---| |---|---|---|---|---|
| `LLM-Research/Llama-4-Scout-17B-16E-Instruct` | BF16 | TP8 | 8 | 8091 | | `Llama-4-Scout-17B-16E-Instruct` | BF16 | TP8 / PP1 | 8 | 8091 |
| `LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8` | FP8 | TP8 + PP2 | 16 | 8092 | | `Llama-4-Maverick-17B-128E-Instruct-FP8` | FP8 | TP8 / PP2 | 16 | 8092 |
--- ---
@@ -24,149 +24,202 @@ llama4-bi150-deploy/
├── Dockerfile ├── Dockerfile
├── entrypoint.sh ├── entrypoint.sh
├── apply_patch.py # 幂等应用全部 Llama 4 补丁 ├── apply_patch.py # 幂等应用全部 Llama 4 补丁
├── detect_model.py # 从 config.json 判断 Scout / Maverick
├── verify.sh # 服务验证脚本
├── patches/ ├── patches/
│ ├── 01_fp8_quant.patch # Maverick FP8 量化 │ ├── 01_fp8_quant.patch # Maverick FP8 量化
│ ├── 02_fp8_moe.patch # Maverick FP8 MoE │ ├── 02_fp8_moe.patch # Maverick FP8 MoE
│ ├── 03_llama4_chunked_attn.patch │ ├── 03_llama4_chunked_attn.patch
│ └── corexx_fp8_moe.py # 逐 expert 反量化 BF16 的 FP8 MoE 回退 │ └── corexx_fp8_moe.py # 逐 expert 反量化 BF16 的 FP8 MoE 回退
├── presets/ ├── run.sh # 自动识别模型 + 构建 + 启动 + 等 /health
│ ├── scout.env ├── docker-compose.yml # 可选
│ └── maverick.env
├── run.sh # 构建 + 启动 + 等 /health
├── docker-compose.yml # 可选,按 profile 启动
├── .gitea/workflows/ ├── .gitea/workflows/
│ ├── docker-build-push.yml │ ├── docker-build-push.yml
│ └── task_info.env │ └── task_info.env
└── README.md └── README.md
``` ```
BI-150 上代码放在:
```text
/root/xjh/llama4-bi150-deploy
```
该路径是 `ugly_hopper` 容器内的 `/root/xjh`,直通宿主机。
--- ---
## 2. 构建与启动 ## 2. 快速开始
### 2.1 脚本(推荐) 不需要传 `scout` / `maverick` 参数,`run.sh` 会读取模型目录里的 `config.json` 自动判断。
```bash ```bash
# Scout:8 卡,8091 cd /root/xjh/llama4-bi150-deploy
./run.sh scout
# Maverick:16 卡,8092 # Scout
./run.sh maverick ./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct
# Maverick
./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
# 也可以用环境变量
MODEL_HOST_DIR=/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8 ./run.sh
# 不传参数时,依次尝试两个默认模型目录
./run.sh
``` ```
`run.sh` 会先 `docker build`,再删除同名容器、启动新容器,并等待 `/health` 最多 10 分钟。 启动后会打印检测结果,例如:
### 2.2 指定 GPU 和模型目录 ```text
==> detected maverick: Llama 4 Maverick 17B-128E Instruct FP8 (TP8+PP2, 16 GPUs)
model : /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
TP/PP : 8/2 (16 GPUs), port=8092, name=llama4-maverick
GPUs : 0,1,2,...,15
```
所有预设值都可以用环境变量覆盖: ### 2.1 自动检测规则
`detect_model.py` 读取 `config.json`:
- `model_type` 必须是 `llama4`;
- `text_config.num_local_experts == 16` → **Scout**,TP8 / PP1 / 8091;
- `text_config.num_local_experts == 128` → **Maverick**,TP8 / PP2 / 8092;
- 字段缺失时用目录名里的 `scout` / `maverick` 兜底;
- 其他情况直接报错,不会用错误的 TP/PP 启动。
容器启动时 `entrypoint.sh` 也会再检测一次。也就是说:
- 用 `run.sh` 启动:宿主侧先检测,决定 GPU 数、容器名、健康检查端口;
- 直接 `docker run` 挂载模型:容器内也会自动检测,按对应 TP/PP/端口启动;
- 显式设置的 `TP` / `PP` / `PORT` / `SERVED_NAME` 会覆盖自动检测;
- 设置 `AUTO_DETECT=0` 可关闭容器内检测;未显式设置的项仍使用 `run.sh` 在宿主侧检测出的默认值。
### 2.2 指定 GPU
默认使用前 `TP*PP` 张卡:Scout 用 `0-7`,Maverick 用 `0-15`。可以覆盖:
```bash ```bash
# Scout 使用 8-15 号卡 # Scout 使用 8-15
GPUS=8,9,10,11,12,13,14,15 ./run.sh scout GPUS=8,9,10,11,12,13,14,15 \
./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct
# Maverick 使用 0-15 号卡 # Maverick 使用 0-15(默认)
GPUS=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ./run.sh maverick GPUS=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 \
./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
```
# 模型放在其他目录 `run.sh` 会检查 GPU 数是否少于 `TP*PP`;少于时直接报错。
MODEL_HOST_DIR=/mnt/disk1/models/Llama-4-Scout-17B-16E-Instruct ./run.sh scout
# 额外挂载一个目录 ### 2.3 额外挂载 / 额外 vLLM 参数
EXTRA_DOCKER_ARGS="-v /mnt/disk1:/data:ro" ./run.sh scout
# 传递额外 vLLM 参数 ```bash
EXTRA_ARGS="--max-model-len 8192" ./run.sh scout EXTRA_DOCKER_ARGS="-v /mnt/disk1:/data:ro" ./run.sh /path/to/model
EXTRA_ARGS="--max-model-len 8192" ./run.sh /path/to/model
# 已有镜像,不重新构建 # 已有镜像,不重新构建
BUILD=0 IMAGE=registry.example.com/llama4-bi150:v1.0.0 ./run.sh scout BUILD=0 IMAGE=registry.example.com/llama4-bi150:v1.0.0 ./run.sh /path/to/model
# 只打印 docker run 命令,便于检查 # 只打印 docker run 命令
DRY_RUN=1 ./run.sh scout DRY_RUN=1 ./run.sh /path/to/model
``` ```
### 2.3 Docker Compose(可选) ### 2.4 Docker Compose(可选)
Compose 使用单个 `llama4` service,同样由 entrypoint 自动检测模型:
```bash ```bash
docker compose --profile scout up -d --build # Scout
docker compose --profile maverick up -d --build MODEL_HOST_DIR=/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct \
docker compose up -d --build
# 覆盖模型目录 / GPU # Maverick
SCOUT_MODEL_HOST_DIR=/mnt/disk1/models/scout SCOUT_GPUS=8,9,10,11,12,13,14,15 \ MODEL_HOST_DIR=/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8 \
docker compose --profile scout up -d --build docker compose up -d --build
# 覆盖镜像 tag / 额外 vLLM 参数 # 指定 GPU / 端口 / 额外参数
TAG=v1.0.0 SCOUT_EXTRA_ARGS="--max-model-len 8192" \ MODEL_HOST_DIR=/mnt/disk0/models/... \
docker compose --profile scout up -d --build GPUS=8,9,10,11,12,13,14,15 PORT=8095 \
docker compose up -d --build
``` ```
--- ---
## 3. 可配置项 ## 3. 可配置项
| 变量 | Scout 默认 | Maverick 默认 | 说明 | | 变量 | 默认 | 说明 |
|---|---|---|---| |---|---|---|
| `IMAGE` | `llama4-bi150:2026-10-26` | 同左 | 镜像名 | | `MODEL_HOST_DIR` | 自动尝试两个默认目录 | 宿主机模型目录 |
| `NAME` | `llama4-scout` | `llama4-maverick` | 容器名 | | `MODEL_MOUNT` | `/models/llama4` | 容器内挂载点 |
| `PORT` | `8091` | `8092` | 宿主机端口 | | `IMAGE` | `llama4-bi150:2026-10-26` | 镜像名 |
| `MODEL_HOST_DIR` | `.../Llama-4-Scout-17B-16E-Instruct` | `.../Llama-4-Maverick-17B-128E-Instruct-FP8` | 宿主机模型目录 | | `NAME` | `llama4-<kind>` | 容器名 |
| `MODEL_MOUNT` | `/models/llama4` | 同左 | 容器内挂载点 | | `PORT` | Scout 8091 / Maverick 8092 | 宿主机端口 |
| `SERVED_NAME` | `llama4-scout` | `llama4-maverick` | OpenAI API 模型名 | | `SERVED_NAME` | Scout `llama4-scout` / Maverick `llama4-maverick` | OpenAI API 模型名 |
| `TP` | `8` | `8` | 张量并行 | | `TP` | Scout 8 / Maverick 8 | 张量并行 |
| `PP` | `1` | `2` | 流水线并行 | | `PP` | Scout 1 / Maverick 2 | 流水线并行 |
| `GPUS` | `0,...,7` | `0,...,15` | 传给 `CUDA_VISIBLE_DEVICES` | | `GPUS` | 前 `TP*PP` 张卡 | `CUDA_VISIBLE_DEVICES` |
| `MAX_MODEL_LEN` | `4096` | `4096` | 最大上下文 | | `MAX_MODEL_LEN` | `4096` | 最大上下文 |
| `MAX_NUM_SEQS` | `1` | `1` | 最大并发 | | `MAX_NUM_SEQS` | `1` | 最大并发 |
| `GPU_MEM_UTIL` | `0.90` | `0.90` | 显存利用率 | | `GPU_MEM_UTIL` | `0.90` | 显存利用率 |
| `ENFORCE_EAGER` | `1` | `1` | 是否 `--enforce-eager` | | `ENFORCE_EAGER` | `1` | 是否 `--enforce-eager` |
| `EXTRA_ARGS` | 空 | 空 | 额外 vLLM 参数 | | `AUTO_DETECT` | `1` | 容器内是否按 config 自动检测 |
| `EXTRA_DOCKER_ARGS` | 空 | 空 | 额外 docker 参数,如 `-v` | | `EXTRA_ARGS` | 空 | 额外 vLLM 参数 |
| `BUILD` | `1` | `1` | `0` 表示跳过构建 | | `EXTRA_DOCKER_ARGS` | 空 | 额外 docker 参数,如 `-v` |
| `DRY_RUN` | `0` | `0` | `1` 只打印命令 | | `BUILD` | `1` | `0` 表示跳过构建 |
| `DRY_RUN` | `0` | `1` 只打印命令 |
`GPUS` 也可以直接用 `CUDA_VISIBLE_DEVICES` 设置。`run.sh` 会检查 GPU 数是否少于 `TP*PP`,少于时直接报错。 `TP` / `PP` / `PORT` / `SERVED_NAME` 显式设置时优先于自动检测。
--- ---
## 4. 验证 ## 4. 验证脚本
`verify.sh` 用于检查已经启动的服务:
```bash ```bash
# 健康检查 ./verify.sh
curl -s http://127.0.0.1:8091/health
curl -s http://127.0.0.1:8092/health
# 模型列表
curl -s http://127.0.0.1:8091/v1/models | python3 -m json.tool
curl -s http://127.0.0.1:8092/v1/models | python3 -m json.tool
# 文本
curl -s http://127.0.0.1:8091/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"llama4-scout","messages":[{"role":"user","content":"你好"}],"max_tokens":64}'
``` ```
Maverick 图片输入测试: 它会自动查找运行中的 `llama4-bi150` 容器,读取模型挂载、端口和 served name,然后检查:
```python 1. `/health`
from PIL import Image 2. `/v1/models`
import base64, io, requests 3. `/v1/chat/completions`(中文问答)
img = Image.new("RGB", (336, 336), (220, 20, 20)) 常用用法:
buf = io.BytesIO(); img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode()
payload = { ```bash
"model": "llama4-maverick", # 指定模型目录(推荐,能准确判断 Scout/Maverick)
"messages": [{"role": "user", "content": [ ./verify.sh --model-dir /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
{"type": "text", "text": "这张图片主要是什么颜色?只回答颜色。"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}}, # 指定容器
]}], ./verify.sh --container llama4-maverick
"max_tokens": 32,
"temperature": 0, # 额外检查图片识别(Maverick)
} ./verify.sh --container llama4-maverick --with-image
print(requests.post("http://127.0.0.1:8092/v1/chat/completions",
json=payload, timeout=300).json()["choices"][0]["message"]["content"]) # 额外检查简单推理(期望回答 27)
./verify.sh --container llama4-scout --with-reasoning
# 直接指定服务地址
./verify.sh --base-url http://127.0.0.1:8092 --served-name llama4-maverick
``` ```
首次使用统一镜像时,建议把 Scout 和 Maverick 都跑一遍,确认 FP8 补丁没有影响 Scout 的 BF16 路径。 主要参数:
| 参数 | 说明 |
|---|---|
| `--model-dir PATH` | 模型目录,用于自动判断 Scout/Maverick |
| `--container NAME` | 容器名;默认自动查找 |
| `--port PORT` | 服务端口;默认从模型/容器推断 |
| `--served-name NAME` | API 模型名;默认从模型/容器推断 |
| `--base-url URL` | 直接指定服务地址 |
| `--timeout SEC` | 等待/请求超时,默认 180 |
| `--with-reasoning` | 额外跑推理题 |
| `--with-image` | 额外跑图片识别(仅 Maverick) |
| `--skip-chat` | 只检查 health 和 models |
脚本只依赖 `bash`、`curl`、`python3`;不需要 `jq`。返回码为 0 表示全部通过。
--- ---
@@ -204,9 +257,7 @@ TASK_TYPE=text-generation
Maverick 支持图片输入;如果 ModelHub 要求把图文任务单独归类,请把 `TASK_TYPE` 改成平台支持的值。`image-verify/validate` 会在构建前校验,填错会直接失败。 Maverick 支持图片输入;如果 ModelHub 要求把图文任务单独归类,请把 `TASK_TYPE` 改成平台支持的值。`image-verify/validate` 会在构建前校验,填错会直接失败。
Runner 需要在 Gitea 实例或仓库中注册,并带有 `amd64-ubuntu-24.04` label。 Runner 需要在 Gitea 实例或仓库中注册,并带有 `amd64-ubuntu-24.04` label。Runner 需要配置以下环境变量(不是仓库 secret):
Runner 需要配置以下环境变量(不是仓库 secret):
```text ```text
DOCKER_REGISTRY DOCKER_REGISTRY
@@ -232,3 +283,4 @@ FIXED_TOKEN
- 补丁是针对当前精确基础镜像生成的。更换基础镜像后不要直接复用,需要重新生成 diff 并在目标环境验证。 - 补丁是针对当前精确基础镜像生成的。更换基础镜像后不要直接复用,需要重新生成 diff 并在目标环境验证。
- `--privileged --pid host -v /dev:/dev` 是 Iluvatar 运行时要求;只给 `/dev/iluvatar*` 可能报 `cudaGetDeviceCount() ... Error 1001`。 - `--privileged --pid host -v /dev:/dev` 是 Iluvatar 运行时要求;只给 `/dev/iluvatar*` 可能报 `cudaGetDeviceCount() ... Error 1001`。
- 模型目录只读挂载,容器不会修改宿主机模型文件。 - 模型目录只读挂载,容器不会修改宿主机模型文件。
- 统一镜像首次使用时,建议 Scout 和 Maverick 都各跑一次 `verify.sh`;尤其是 Scout,因为镜像里也包含 FP8 MoE 补丁,需要确认共享的 `fused_moe.py` 修改没有影响 BF16 路径。

145
detect_model.py Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Detect a Llama 4 variant from its config.json.
The unified image can serve both:
- Llama-4-Scout-17B-16E-Instruct (16 local experts, BF16) -> TP8, PP1, 8 GPUs
- Llama-4-Maverick-17B-128E-Instruct-FP8 (128 local experts, FP8) -> TP8, PP2, 16 GPUs
The detected values are printed as shell assignments by default, so run.sh and
entrypoint.sh can `eval` them. `--json` is used by verify.sh.
Detection is based on `text_config.num_local_experts`, with a model-directory
name fallback if the field is missing. Unsupported configs fail loudly rather
than silently starting with the wrong TP/PP.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import shlex
import sys
PROFILES = {
"scout": {
"tp": 8,
"pp": 1,
"port": 8091,
"served_name": "llama4-scout",
"gpu_count": 8,
"description": "Llama 4 Scout 17B-16E Instruct (BF16, TP8, 8 GPUs)",
},
"maverick": {
"tp": 8,
"pp": 2,
"port": 8092,
"served_name": "llama4-maverick",
"gpu_count": 16,
"description": "Llama 4 Maverick 17B-128E Instruct FP8 (TP8+PP2, 16 GPUs)",
},
}
def load_config(model_dir: str) -> dict:
path = pathlib.Path(model_dir) / "config.json"
if not path.is_file():
raise ValueError(f"config.json not found: {path}")
try:
return json.loads(path.read_text())
except json.JSONDecodeError as exc:
raise ValueError(f"invalid config.json: {exc}") from exc
def detect(model_dir: str) -> dict:
cfg = load_config(model_dir)
model_type = str(cfg.get("model_type", ""))
if model_type != "llama4":
raise ValueError(
f"unsupported model_type={model_type!r}; expected 'llama4'"
)
text = cfg.get("text_config") or {}
experts = text.get("num_local_experts")
if experts is None:
experts = text.get("num_experts")
if experts is not None:
experts = int(experts)
if experts >= 64:
kind = "maverick"
elif experts <= 32:
kind = "scout"
else:
raise ValueError(
f"cannot determine Llama 4 variant: num_local_experts={experts}; "
"expected 16 (Scout) or 128 (Maverick)"
)
detection = f"num_local_experts={experts}"
else:
name = pathlib.Path(model_dir).name.lower()
if "maverick" in name:
kind = "maverick"
elif "scout" in name:
kind = "scout"
else:
raise ValueError(
"cannot determine Llama 4 variant: text_config.num_local_experts "
"is missing and the directory name does not contain scout/maverick"
)
detection = "directory name"
profile = dict(PROFILES[kind])
profile.update(
{
"kind": kind,
"model_type": model_type,
"architecture": (cfg.get("architectures") or [""])[0],
"num_local_experts": experts,
"quantized": bool(cfg.get("quantization_config")),
"detection": detection,
"model_dir": str(pathlib.Path(model_dir).resolve()),
}
)
return profile
def main() -> int:
parser = argparse.ArgumentParser(
description="Detect Llama 4 Scout/Maverick from config.json"
)
parser.add_argument("model_dir", help="directory containing config.json")
parser.add_argument(
"--json",
action="store_true",
help="print JSON instead of shell assignments",
)
args = parser.parse_args()
try:
profile = detect(args.model_dir)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if args.json:
print(json.dumps(profile, ensure_ascii=False, indent=2))
return 0
values = {
"DETECTED_MODEL_KIND": profile["kind"],
"DETECTED_TP": profile["tp"],
"DETECTED_PP": profile["pp"],
"DETECTED_PORT": profile["port"],
"DETECTED_SERVED_NAME": profile["served_name"],
"DETECTED_GPU_COUNT": profile["gpu_count"],
"DETECTED_DESCRIPTION": profile["description"],
}
for key, value in values.items():
print(f"{key}={shlex.quote(str(value))}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,54 +1,46 @@
x-llama4-common: &llama4-common
build:
context: .
dockerfile: Dockerfile
image: llama4-bi150:${TAG:-2026-10-26}
network_mode: host
ipc: host
privileged: true
pid: host
ulimits:
memlock: -1
stack: 67108864
restart: unless-stopped
services: services:
scout: llama4:
<<: *llama4-common build:
profiles: ["scout"] context: .
container_name: llama4-scout dockerfile: Dockerfile
volumes: image: llama4-bi150:${TAG:-2026-10-26}
- /dev:/dev container_name: llama4
- ${SCOUT_MODEL_HOST_DIR:-/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct}:/models/llama4:ro
environment: # vLLM 多卡 NCCL / 端口直通
MODEL_PATH: /models/llama4 network_mode: host
SERVED_NAME: llama4-scout ipc: host
PORT: "8091"
TP: "8" # Iluvatar 运行时要求
PP: "1" privileged: true
MAX_MODEL_LEN: "4096" pid: host
MAX_NUM_SEQS: "1"
GPU_MEM_UTIL: "0.90"
ENFORCE_EAGER: "1"
CUDA_VISIBLE_DEVICES: "${SCOUT_GPUS:-0,1,2,3,4,5,6,7}"
EXTRA_ARGS: "${SCOUT_EXTRA_ARGS:-}"
maverick:
<<: *llama4-common
profiles: ["maverick"]
container_name: llama4-maverick
volumes: volumes:
- /dev:/dev - /dev:/dev
- ${MAVERICK_MODEL_HOST_DIR:-/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8}:/models/llama4:ro - ${MODEL_HOST_DIR:-/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct}:/models/llama4:ro
environment: environment:
MODEL_PATH: /models/llama4 MODEL_PATH: /models/llama4
SERVED_NAME: llama4-maverick AUTO_DETECT: "1"
PORT: "8092" HOST: 0.0.0.0
TP: "8"
PP: "2" # 默认暴露全部 16 张卡;entrypoint 会根据模型选择实际 TP*PP。
MAX_MODEL_LEN: "4096" CUDA_VISIBLE_DEVICES: "${GPUS:-0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}"
MAX_NUM_SEQS: "1"
GPU_MEM_UTIL: "0.90" # 留空时由 entrypoint 根据 config.json 自动判断。
ENFORCE_EAGER: "1" # 如果需要手动覆盖,取消注释并填值。
CUDA_VISIBLE_DEVICES: "${MAVERICK_GPUS:-0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}" # TP: "${TP:-}"
EXTRA_ARGS: "${MAVERICK_EXTRA_ARGS:-}" # PP: "${PP:-}"
# PORT: "${PORT:-}"
# SERVED_NAME: "${SERVED_NAME:-}"
MAX_MODEL_LEN: "${MAX_MODEL_LEN:-4096}"
MAX_NUM_SEQS: "${MAX_NUM_SEQS:-1}"
GPU_MEM_UTIL: "${GPU_MEM_UTIL:-0.90}"
ENFORCE_EAGER: "${ENFORCE_EAGER:-1}"
EXTRA_ARGS: "${EXTRA_ARGS:-}"
ulimits:
memlock: -1
stack: 67108864
restart: unless-stopped

View File

@@ -1,29 +1,24 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Llama 4 Scout / Maverick 统一容器入口: # Llama 4 统一容器入口:
# 1. 幂等应用 CoreX vLLM 的 Llama 4 补丁(Scout 只用 chunked attention, # 1. 幂等应用 CoreX vLLM 的 Llama 4 补丁
# Maverick 额外使用 FP8 补丁;补丁脚本会按 marker 自动跳过已应用项) # 2. 读取 /models/llama4/config.json,自动判断 Scout / Maverick
# 2. 检查模型目录 # 3. 选择对应的 TP / PP / 端口 / served name,再启动 vLLM
# 3. exec vLLM(信号可直接传给 vLLM) #
# AUTO_DETECT=1(默认)时根据模型 config 自动选择;显式设置的
# TP / PP / PORT / SERVED_NAME 会覆盖检测结果。AUTO_DETECT=0 可完全手动。
# #
set -euo pipefail set -euo pipefail
PATCH_SCRIPT="${LLAMA4_PATCH_SCRIPT:-/opt/llama4/apply_patch.py}" PATCH_SCRIPT="${LLAMA4_PATCH_SCRIPT:-/opt/llama4/apply_patch.py}"
DETECT_SCRIPT="${LLAMA4_DETECT_SCRIPT:-/opt/llama4/detect_model.py}"
VLLM_BIN="${VLLM_BIN:-vllm}" VLLM_BIN="${VLLM_BIN:-vllm}"
python3 "${PATCH_SCRIPT}" python3 "${PATCH_SCRIPT}"
MODEL_PATH="${MODEL_PATH:-/models/llama4}" MODEL_PATH="${MODEL_PATH:-/models/llama4}"
SERVED_NAME="${SERVED_NAME:-llama4}" AUTO_DETECT="${AUTO_DETECT:-1}"
HOST="${HOST:-0.0.0.0}" MODEL_DESCRIPTION=""
PORT="${PORT:-8091}"
TP="${TP:-8}"
PP="${PP:-1}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-4096}"
MAX_NUM_SEQS="${MAX_NUM_SEQS:-1}"
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.90}"
ENFORCE_EAGER="${ENFORCE_EAGER:-1}"
EXTRA_ARGS="${EXTRA_ARGS:-}"
if [[ ! -f "${MODEL_PATH}/config.json" ]]; then if [[ ! -f "${MODEL_PATH}/config.json" ]]; then
echo "ERROR: 模型目录不完整:${MODEL_PATH}/config.json 不存在" >&2 echo "ERROR: 模型目录不完整:${MODEL_PATH}/config.json 不存在" >&2
@@ -33,6 +28,30 @@ if [[ ! -f "${MODEL_PATH}/config.json" ]]; then
exit 1 exit 1
fi fi
if [[ "${AUTO_DETECT}" == "1" ]]; then
DETECT_OUTPUT="$(python3 "${DETECT_SCRIPT}" "${MODEL_PATH}")" || exit 1
eval "${DETECT_OUTPUT}"
MODEL_KIND="${MODEL_KIND:-${DETECTED_MODEL_KIND}}"
TP="${TP:-${DETECTED_TP}}"
PP="${PP:-${DETECTED_PP}}"
PORT="${PORT:-${DETECTED_PORT}}"
SERVED_NAME="${SERVED_NAME:-${DETECTED_SERVED_NAME}}"
MODEL_DESCRIPTION="${DETECTED_DESCRIPTION:-}"
else
MODEL_KIND="${MODEL_KIND:-unknown}"
fi
HOST="${HOST:-0.0.0.0}"
TP="${TP:-8}"
PP="${PP:-1}"
PORT="${PORT:-8091}"
SERVED_NAME="${SERVED_NAME:-llama4}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-4096}"
MAX_NUM_SEQS="${MAX_NUM_SEQS:-1}"
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.90}"
ENFORCE_EAGER="${ENFORCE_EAGER:-1}"
EXTRA_ARGS="${EXTRA_ARGS:-}"
PP_ARGS=() PP_ARGS=()
if (( PP > 1 )); then if (( PP > 1 )); then
PP_ARGS=(--pipeline-parallel-size "${PP}") PP_ARGS=(--pipeline-parallel-size "${PP}")
@@ -45,6 +64,7 @@ fi
echo "============================================================" echo "============================================================"
echo "Llama 4 (BI-V150 unified image)" echo "Llama 4 (BI-V150 unified image)"
echo " model kind : ${MODEL_KIND}"
echo " model : ${MODEL_PATH}" echo " model : ${MODEL_PATH}"
echo " served name: ${SERVED_NAME}" echo " served name: ${SERVED_NAME}"
echo " listen : ${HOST}:${PORT}" echo " listen : ${HOST}:${PORT}"
@@ -55,6 +75,9 @@ echo " gpu util : ${GPU_MEM_UTIL}"
echo " eager : ${ENFORCE_EAGER}" echo " eager : ${ENFORCE_EAGER}"
echo " visible : ${CUDA_VISIBLE_DEVICES:-all}" echo " visible : ${CUDA_VISIBLE_DEVICES:-all}"
echo " extra : ${EXTRA_ARGS}" echo " extra : ${EXTRA_ARGS}"
if [[ -n "${MODEL_DESCRIPTION}" ]]; then
echo " detected : ${MODEL_DESCRIPTION}"
fi
echo "============================================================" echo "============================================================"
# shellcheck disable=SC2086 # shellcheck disable=SC2086

View File

@@ -1,12 +0,0 @@
# Llama 4 Maverick FP8 默认配置
DEFAULT_IMAGE="llama4-bi150:2026-10-26"
DEFAULT_NAME="llama4-maverick"
DEFAULT_PORT="8092"
DEFAULT_MODEL_HOST_DIR="/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8"
DEFAULT_SERVED_NAME="llama4-maverick"
DEFAULT_TP="8"
DEFAULT_PP="2"
DEFAULT_GPUS="0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15"
DEFAULT_MAX_MODEL_LEN="4096"
DEFAULT_MAX_NUM_SEQS="1"
DEFAULT_GPU_MEM_UTIL="0.90"

View File

@@ -1,12 +0,0 @@
# Llama 4 Scout 默认配置
DEFAULT_IMAGE="llama4-bi150:2026-10-26"
DEFAULT_NAME="llama4-scout"
DEFAULT_PORT="8091"
DEFAULT_MODEL_HOST_DIR="/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct"
DEFAULT_SERVED_NAME="llama4-scout"
DEFAULT_TP="8"
DEFAULT_PP="1"
DEFAULT_GPUS="0,1,2,3,4,5,6,7"
DEFAULT_MAX_MODEL_LEN="4096"
DEFAULT_MAX_NUM_SEQS="1"
DEFAULT_GPU_MEM_UTIL="0.90"

128
run.sh
View File

@@ -1,17 +1,24 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Llama 4 Scout / Maverick 一键构建 + 启动(Iluvatar BI-V150) # Llama 4 统一构建 + 启动(Iluvatar BI-V150)
# #
# ./run.sh scout # 自动读取模型目录下的 config.json,判断是 Scout 还是 Maverick,然后选择:
# ./run.sh maverick # Scout : TP8 / PP1 / 8 卡 / 8091 / llama4-scout
# Maverick : TP8 / PP2 / 16 卡 / 8092 / llama4-maverick
#
# 用法:
# ./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct
# ./run.sh /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
# MODEL_HOST_DIR=/path/to/model ./run.sh
# ./run.sh # 依次尝试两个默认模型目录
# #
# 常用覆盖项: # 常用覆盖项:
# GPUS=8,9,10,11,12,13,14,15 ./run.sh scout # GPUS=8,9,10,11,12,13,14,15 ./run.sh /path/to/scout
# MODEL_HOST_DIR=/path/to/model ./run.sh maverick # EXTRA_DOCKER_ARGS="-v /mnt/disk1:/data:ro" ./run.sh /path/to/model
# EXTRA_ARGS="--max-model-len 8192" ./run.sh scout # EXTRA_ARGS="--max-model-len 8192" ./run.sh /path/to/model
# EXTRA_DOCKER_ARGS="-v /mnt/disk1:/data:ro" ./run.sh scout # TP=8 PP=1 PORT=8095 SERVED_NAME=my-llama4 ./run.sh /path/to/model
# BUILD=0 ./run.sh scout # 使用已有镜像,不重新构建 # BUILD=0 ./run.sh /path/to/model # 使用已有镜像
# DRY_RUN=1 ./run.sh scout # 只打印 docker run 命令 # DRY_RUN=1 ./run.sh /path/to/model # 只打印 docker run 命令
# #
set -euo pipefail set -euo pipefail
@@ -19,67 +26,91 @@ cd "$(dirname "$0")"
usage() { usage() {
cat >&2 <<'EOF' cat >&2 <<'EOF'
用法: ./run.sh <scout|maverick> 用法: ./run.sh [MODEL_DIR]
预设默认值在 presets/<name>.env 中,所有值都可以用环境变量覆盖: 不传 MODEL_DIR 时,按顺序尝试:
IMAGE NAME PORT MODEL_HOST_DIR MODEL_MOUNT SERVED_NAME /mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct
/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8
模型类型由 MODEL_DIR/config.json 自动判断,不需要 scout/maverick 参数。
环境变量覆盖:
IMAGE NAME MODEL_HOST_DIR MODEL_MOUNT PORT SERVED_NAME
TP PP GPUS MAX_MODEL_LEN MAX_NUM_SEQS GPU_MEM_UTIL TP PP GPUS MAX_MODEL_LEN MAX_NUM_SEQS GPU_MEM_UTIL
ENFORCE_EAGER EXTRA_ARGS EXTRA_DOCKER_ARGS ENFORCE_EAGER AUTO_DETECT EXTRA_ARGS EXTRA_DOCKER_ARGS
BUILD=0/1 DRY_RUN=0/1 BUILD=0/1 DRY_RUN=0/1
EOF EOF
} }
VARIANT="${1:-}" if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
if [[ -z "${VARIANT}" || "${VARIANT}" == "-h" || "${VARIANT}" == "--help" ]]; then usage
exit 0
fi
MODEL_HOST_DIR="${1:-${MODEL_HOST_DIR:-}}"
if [[ -z "${MODEL_HOST_DIR}" ]]; then
for candidate in \
"/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct" \
"/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8"; do
if [[ -f "${candidate}/config.json" ]]; then
MODEL_HOST_DIR="${candidate}"
break
fi
done
fi
if [[ -z "${MODEL_HOST_DIR}" ]]; then
echo "ERROR: 未指定模型目录,也没有找到默认模型目录。" >&2
usage usage
exit 2 exit 2
fi fi
PRESET="presets/${VARIANT}.env" if [[ ! -f "${MODEL_HOST_DIR}/config.json" ]]; then
if [[ ! -f "${PRESET}" ]]; then echo "ERROR: 模型目录不完整: ${MODEL_HOST_DIR}/config.json" >&2
echo "ERROR: 未知预设: ${VARIANT}" >&2 echo "请先下载模型,或把 MODEL_DIR 指向正确目录。" >&2
usage exit 1
exit 2
fi fi
# shellcheck disable=SC1090 DETECT_OUTPUT="$(python3 detect_model.py "${MODEL_HOST_DIR}")" || exit 1
source "${PRESET}" eval "${DETECT_OUTPUT}"
IMAGE="${IMAGE:-${DEFAULT_IMAGE}}" MODEL_KIND="${DETECTED_MODEL_KIND}"
NAME="${NAME:-${DEFAULT_NAME}}" IMAGE="${IMAGE:-llama4-bi150:2026-10-26}"
PORT="${PORT:-${DEFAULT_PORT}}" NAME="${NAME:-llama4-${MODEL_KIND}}"
MODEL_HOST_DIR="${MODEL_HOST_DIR:-${DEFAULT_MODEL_HOST_DIR}}" PORT="${PORT:-${DETECTED_PORT}}"
MODEL_MOUNT="${MODEL_MOUNT:-/models/llama4}" MODEL_MOUNT="${MODEL_MOUNT:-/models/llama4}"
SERVED_NAME="${SERVED_NAME:-${DEFAULT_SERVED_NAME}}" SERVED_NAME="${SERVED_NAME:-${DETECTED_SERVED_NAME}}"
TP="${TP:-${DEFAULT_TP}}" TP="${TP:-${DETECTED_TP}}"
PP="${PP:-${DEFAULT_PP}}" PP="${PP:-${DETECTED_PP}}"
GPUS="${GPUS:-${CUDA_VISIBLE_DEVICES:-${DEFAULT_GPUS}}}" MAX_MODEL_LEN="${MAX_MODEL_LEN:-4096}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-${DEFAULT_MAX_MODEL_LEN}}" MAX_NUM_SEQS="${MAX_NUM_SEQS:-1}"
MAX_NUM_SEQS="${MAX_NUM_SEQS:-${DEFAULT_MAX_NUM_SEQS}}" GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.90}"
GPU_MEM_UTIL="${GPU_MEM_UTIL:-${DEFAULT_GPU_MEM_UTIL}}"
ENFORCE_EAGER="${ENFORCE_EAGER:-1}" ENFORCE_EAGER="${ENFORCE_EAGER:-1}"
AUTO_DETECT="${AUTO_DETECT:-1}"
EXTRA_ARGS="${EXTRA_ARGS:-}" EXTRA_ARGS="${EXTRA_ARGS:-}"
EXTRA_DOCKER_ARGS="${EXTRA_DOCKER_ARGS:-}" EXTRA_DOCKER_ARGS="${EXTRA_DOCKER_ARGS:-}"
BUILD="${BUILD:-1}" BUILD="${BUILD:-1}"
DRY_RUN="${DRY_RUN:-0}" DRY_RUN="${DRY_RUN:-0}"
GPU_COUNT=$((TP * PP))
if [[ -n "${GPUS:-}" ]]; then
GPUS="${GPUS}"
elif [[ -n "${CUDA_VISIBLE_DEVICES:-}" ]]; then
GPUS="${CUDA_VISIBLE_DEVICES}"
else
GPUS="$(seq -s, 0 $((GPU_COUNT - 1)))"
fi
GPUS="${GPUS// /}" GPUS="${GPUS// /}"
if [[ ! -f "${MODEL_HOST_DIR}/config.json" ]]; then
echo "ERROR: 模型目录不完整: ${MODEL_HOST_DIR}/config.json" >&2
echo "请先下载模型,或设置 MODEL_HOST_DIR 指向正确目录。" >&2
exit 1
fi
EXPECTED_GPUS=$((TP * PP))
if [[ -n "${GPUS}" ]]; then if [[ -n "${GPUS}" ]]; then
IFS=',' read -r -a GPU_LIST <<< "${GPUS}" IFS=',' read -r -a GPU_LIST <<< "${GPUS}"
if (( ${#GPU_LIST[@]} < EXPECTED_GPUS )); then if (( ${#GPU_LIST[@]} < GPU_COUNT )); then
echo "ERROR: TP*PP=${EXPECTED_GPUS},但 GPUS 只给了 ${#GPU_LIST[@]} 张: ${GPUS}" >&2 echo "ERROR: ${MODEL_KIND} 需要 TP*PP=${GPU_COUNT} 张卡,但 GPUS 只给了 ${#GPU_LIST[@]} 张: ${GPUS}" >&2
exit 1 exit 1
fi fi
if (( ${#GPU_LIST[@]} > EXPECTED_GPUS )); then if (( ${#GPU_LIST[@]} > GPU_COUNT )); then
echo "WARN: TP*PP=${EXPECTED_GPUS},但 GPUS 给了 ${#GPU_LIST[@]} 张;vLLM 只会使用其中 ${EXPECTED_GPUS} 张。" >&2 echo "WARN: TP*PP=${GPU_COUNT},但 GPUS 给了 ${#GPU_LIST[@]} 张;vLLM 只会使用其中 ${GPU_COUNT} 张。" >&2
fi fi
fi fi
@@ -101,6 +132,8 @@ DOCKER_RUN=(
--ulimit stack=67108864 --ulimit stack=67108864
-e CUDA_VISIBLE_DEVICES="${GPUS}" -e CUDA_VISIBLE_DEVICES="${GPUS}"
-e MODEL_PATH="${MODEL_MOUNT}" -e MODEL_PATH="${MODEL_MOUNT}"
-e AUTO_DETECT="${AUTO_DETECT}"
-e MODEL_KIND="${MODEL_KIND}"
-e SERVED_NAME="${SERVED_NAME}" -e SERVED_NAME="${SERVED_NAME}"
-e PORT="${PORT}" -e PORT="${PORT}"
-e TP="${TP}" -e TP="${TP}"
@@ -115,6 +148,11 @@ DOCKER_RUN=(
"${IMAGE}" "${IMAGE}"
) )
echo "==> detected ${MODEL_KIND}: ${DETECTED_DESCRIPTION}"
echo " model : ${MODEL_HOST_DIR}"
echo " TP/PP : ${TP}/${PP} (${GPU_COUNT} GPUs), port=${PORT}, name=${SERVED_NAME}"
echo " GPUs : ${GPUS}"
if [[ "${DRY_RUN}" == "1" ]]; then if [[ "${DRY_RUN}" == "1" ]]; then
printf '%q ' "${DOCKER_RUN[@]}" printf '%q ' "${DOCKER_RUN[@]}"
printf '\n' printf '\n'
@@ -131,7 +169,7 @@ fi
echo "==> recreate ${NAME}" echo "==> recreate ${NAME}"
docker rm -f "${NAME}" 2>/dev/null || true docker rm -f "${NAME}" 2>/dev/null || true
echo "==> run ${VARIANT}: GPUs=${GPUS}, TP=${TP}, PP=${PP}, model=${MODEL_HOST_DIR}" echo "==> run ${MODEL_KIND}"
"${DOCKER_RUN[@]}" "${DOCKER_RUN[@]}"
echo "==> wait for http://127.0.0.1:${PORT}/health" echo "==> wait for http://127.0.0.1:${PORT}/health"

355
verify.sh Executable file
View File

@@ -0,0 +1,355 @@
#!/usr/bin/env bash
#
# Llama 4 Scout / Maverick 服务验证脚本
#
# 默认行为:
# 1. 如果 docker 可用,自动找正在运行的 llama4-bi150 容器;
# 2. 从容器挂载/环境变量或 MODEL_HOST_DIR 推断模型目录、端口、served name;
# 3. 如果模型目录可读,调用 detect_model.py 自动判断 Scout / Maverick;
# 4. 依次检查 /health、/v1/models、/v1/chat/completions;
# 5. 可选 --with-reasoning 和 --with-image。
#
# 常用:
# ./verify.sh
# ./verify.sh --model-dir /mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8 --with-image
# ./verify.sh --container llama4-scout --with-reasoning
# ./verify.sh --base-url http://127.0.0.1:8092 --served-name llama4-maverick
#
set -euo pipefail
cd "$(dirname "$0")"
MODEL_DIR="${MODEL_HOST_DIR:-}"
CONTAINER="${CONTAINER:-}"
PORT="${PORT:-}"
SERVED_NAME="${SERVED_NAME:-}"
BASE_URL="${BASE_URL:-}"
TIMEOUT="${TIMEOUT:-180}"
WITH_IMAGE=0
WITH_REASONING=0
SKIP_CHAT=0
KIND="unknown"
usage() {
cat >&2 <<'EOF'
用法: ./verify.sh [options]
--model-dir PATH 模型目录(含 config.json),用于自动判断 Scout/Maverick
--container NAME 运行中的容器名;默认自动查找 llama4-bi150 容器
--port PORT 服务端口;默认从模型/容器推断
--served-name NAME OpenAI API 模型名;默认从模型/容器推断
--base-url URL 直接指定服务地址,例如 http://127.0.0.1:8092
--timeout SEC 等待和请求超时,默认 180
--with-reasoning 额外跑一个简单推理题(期望 27)
--with-image 额外跑图片识别(仅 Maverick 有意义)
--skip-chat 只检查 /health 和 /v1/models
-h, --help 显示帮助
环境变量也可以设置:MODEL_HOST_DIR CONTAINER PORT SERVED_NAME BASE_URL TIMEOUT
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--model-dir) MODEL_DIR="${2:?--model-dir 需要路径}"; shift 2 ;;
--container) CONTAINER="${2:?--container 需要名称}"; shift 2 ;;
--port) PORT="${2:?--port 需要端口}"; shift 2 ;;
--served-name) SERVED_NAME="${2:?--served-name 需要名称}"; shift 2 ;;
--base-url) BASE_URL="${2:?--base-url 需要 URL}"; shift 2 ;;
--timeout) TIMEOUT="${2:?--timeout 需要秒数}"; shift 2 ;;
--with-image) WITH_IMAGE=1; shift ;;
--with-reasoning) WITH_REASONING=1; shift ;;
--skip-chat) SKIP_CHAT=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: 未知参数: $1" >&2; usage; exit 2 ;;
esac
done
fail() { echo "FAIL: $*" >&2; exit 1; }
pass() { echo "PASS: $*"; }
command -v curl >/dev/null 2>&1 || fail "缺少 curl"
command -v python3 >/dev/null 2>&1 || fail "缺少 python3"
TMP="$(mktemp -d)"
trap 'rm -rf "${TMP}"' EXIT
# ---------------------------------------------------------------- container
if [[ -z "${CONTAINER}" ]] && command -v docker >/dev/null 2>&1; then
CONTAINER="$(docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null \
| awk '/llama4-bi150|llama4-scout|llama4-maverick/ {print $1; exit}' || true)"
fi
container_env() {
local name="$1" key="$2"
docker inspect "${name}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
| awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' || true
}
if [[ -n "${CONTAINER}" ]] && command -v docker >/dev/null 2>&1; then
if [[ -z "${MODEL_DIR}" ]]; then
MODEL_DIR="$(docker inspect "${CONTAINER}" --format '{{json .Mounts}}' 2>/dev/null \
| python3 -c '
import json, sys
try:
mounts = json.load(sys.stdin)
except Exception:
sys.exit(0)
for m in mounts:
if m.get("Destination") == "/models/llama4":
print(m.get("Source", ""))
break
' || true)"
fi
[[ -z "${PORT}" ]] && PORT="$(container_env "${CONTAINER}" PORT)"
[[ -z "${SERVED_NAME}" ]] && SERVED_NAME="$(container_env "${CONTAINER}" SERVED_NAME)"
fi
# ---------------------------------------------------------------- model dir
if [[ -z "${MODEL_DIR}" ]]; then
for candidate in \
"/mnt/disk0/models/LLM-Research/Llama-4-Scout-17B-16E-Instruct" \
"/mnt/disk0/models/LLM-Research/Llama-4-Maverick-17B-128E-Instruct-FP8"; do
if [[ -f "${candidate}/config.json" ]]; then
MODEL_DIR="${candidate}"
break
fi
done
fi
DETECT_JSON=""
if [[ -n "${MODEL_DIR}" ]]; then
if [[ -f "${MODEL_DIR}/config.json" ]]; then
DETECT_JSON="$(python3 detect_model.py "${MODEL_DIR}" --json)" || {
echo "ERROR: 无法识别模型: ${MODEL_DIR}" >&2
exit 1
}
elif [[ -n "${CONTAINER}" ]] && command -v docker >/dev/null 2>&1; then
# 宿主机路径不可读时,退回到容器内检测
DETECT_JSON="$(docker exec "${CONTAINER}" python3 /opt/llama4/detect_model.py /models/llama4 --json 2>/dev/null || true)"
else
fail "模型目录缺少 config.json: ${MODEL_DIR}"
fi
fi
if [[ -n "${DETECT_JSON}" ]]; then
KIND="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["kind"])' <<< "${DETECT_JSON}")"
DETECTED_PORT="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["port"])' <<< "${DETECT_JSON}")"
DETECTED_NAME="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["served_name"])' <<< "${DETECT_JSON}")"
[[ -z "${PORT}" ]] && PORT="${DETECTED_PORT}"
[[ -z "${SERVED_NAME}" ]] && SERVED_NAME="${DETECTED_NAME}"
fi
PORT="${PORT:-8091}"
SERVED_NAME="${SERVED_NAME:-llama4}"
BASE_URL="${BASE_URL:-http://127.0.0.1:${PORT}}"
BASE_URL="${BASE_URL%/}"
echo "=== Llama 4 verification ==="
echo "container : ${CONTAINER:-<none>}"
echo "model dir : ${MODEL_DIR:-<not detected>}"
echo "kind : ${KIND}"
echo "base URL : ${BASE_URL}"
echo "model name: ${SERVED_NAME}"
echo
# ---------------------------------------------------------------- health
echo "==> waiting for /health (up to ${TIMEOUT}s)"
HEALTH_OK=0
DEADLINE=$((SECONDS + TIMEOUT))
while (( SECONDS < DEADLINE )); do
if curl -fsS --max-time 5 "${BASE_URL}/health" > "${TMP}/health.txt" 2>/dev/null; then
HEALTH_OK=1
break
fi
sleep 2
done
if [[ "${HEALTH_OK}" != "1" ]]; then
fail "health check 超时: ${BASE_URL}/health"
fi
HEALTH="$(cat "${TMP}/health.txt" 2>/dev/null || true)"
pass "health: ${HEALTH:-OK}"
# ---------------------------------------------------------------- models
echo "==> /v1/models"
curl -fsS --max-time "${TIMEOUT}" "${BASE_URL}/v1/models" > "${TMP}/models.json" \
|| fail "GET /v1/models 失败"
python3 - "${TMP}/models.json" "${SERVED_NAME}" <<'PY'
import json
import sys
data = json.load(open(sys.argv[1]))
expected = sys.argv[2]
ids = [m.get("id") for m in data.get("data", [])]
if expected not in ids:
print(f"FAIL: /v1/models 中没有 {expected!r}; 实际: {ids}", file=sys.stderr)
sys.exit(1)
for m in data.get("data", []):
if m.get("id") == expected:
print(f"PASS: model={expected}, max_model_len={m.get('max_model_len')}")
break
PY
if [[ "${SKIP_CHAT}" == "1" ]]; then
echo
echo "All checks passed (chat skipped)."
exit 0
fi
# ---------------------------------------------------------------- chat
echo "==> /v1/chat/completions"
python3 - "${SERVED_NAME}" > "${TMP}/chat.json" <<'PY'
import json
import sys
print(json.dumps({
"model": sys.argv[1],
"messages": [{"role": "user", "content": "你好,请用一句中文介绍你自己。"}],
"max_tokens": 64,
"temperature": 0,
}, ensure_ascii=False))
PY
curl -fsS --max-time "${TIMEOUT}" \
-H 'Content-Type: application/json' \
--data-binary @"${TMP}/chat.json" \
"${BASE_URL}/v1/chat/completions" > "${TMP}/chat_resp.json" \
|| fail "chat completion 失败"
python3 - "${TMP}/chat_resp.json" <<'PY'
import json
import sys
data = json.load(open(sys.argv[1]))
try:
content = data["choices"][0]["message"]["content"]
except (KeyError, IndexError) as exc:
print(f"FAIL: unexpected chat response: {data}", file=sys.stderr)
sys.exit(1)
if isinstance(content, list):
content = " ".join(
part.get("text", "") for part in content if isinstance(part, dict)
)
content = (content or "").strip()
if not content:
print("FAIL: chat response content is empty", file=sys.stderr)
sys.exit(1)
print(f"PASS: chat: {content[:200]}")
PY
# ---------------------------------------------------------------- reasoning
if [[ "${WITH_REASONING}" == "1" ]]; then
echo "==> reasoning"
python3 - "${SERVED_NAME}" > "${TMP}/reason.json" <<'PY'
import json
import sys
print(json.dumps({
"model": sys.argv[1],
"messages": [{"role": "user", "content": "3 个人 3 天喝 3 桶水,9 个人 9 天喝几桶水?只给最终数字。"}],
"max_tokens": 16,
"temperature": 0,
}, ensure_ascii=False))
PY
curl -fsS --max-time "${TIMEOUT}" \
-H 'Content-Type: application/json' \
--data-binary @"${TMP}/reason.json" \
"${BASE_URL}/v1/chat/completions" > "${TMP}/reason_resp.json" \
|| fail "reasoning 请求失败"
python3 - "${TMP}/reason_resp.json" <<'PY'
import json
import sys
data = json.load(open(sys.argv[1]))
content = data["choices"][0]["message"].get("content", "")
if isinstance(content, list):
content = " ".join(
part.get("text", "") for part in content if isinstance(part, dict)
)
content = (content or "").strip()
if "27" not in content:
print(f"FAIL: expected 27, got: {content!r}", file=sys.stderr)
sys.exit(1)
print(f"PASS: reasoning: {content[:100]}")
PY
fi
# ---------------------------------------------------------------- image
if [[ "${WITH_IMAGE}" == "1" ]]; then
if [[ "${KIND}" != "maverick" && "${KIND}" != "unknown" ]]; then
echo "SKIP: image test 仅适用于 Maverick(当前识别为 ${KIND})"
else
echo "==> image (336x336 red PNG)"
python3 - "${TMP}/red.b64" <<'PY'
import base64
import struct
import sys
import zlib
w = h = 336
row = bytes((220, 20, 20)) * w
raw = b"".join(b"\x00" + row for _ in range(h))
def chunk(kind, data):
return (
struct.pack(">I", len(data))
+ kind
+ data
+ struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
)
png = (
b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
+ chunk(b"IDAT", zlib.compress(raw, 9))
+ chunk(b"IEND", b"")
)
open(sys.argv[1], "w").write(base64.b64encode(png).decode())
PY
python3 - "${SERVED_NAME}" "${TMP}/red.b64" > "${TMP}/image.json" <<'PY'
import json
import sys
b64 = open(sys.argv[2]).read().strip()
payload = {
"model": sys.argv[1],
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "这张图片主要是什么颜色?只回答颜色。"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}},
],
}],
"max_tokens": 32,
"temperature": 0,
}
print(json.dumps(payload, ensure_ascii=False))
PY
curl -fsS --max-time "${TIMEOUT}" \
-H 'Content-Type: application/json' \
--data-binary @"${TMP}/image.json" \
"${BASE_URL}/v1/chat/completions" > "${TMP}/image_resp.json" \
|| fail "image 请求失败"
python3 - "${TMP}/image_resp.json" <<'PY'
import json
import sys
data = json.load(open(sys.argv[1]))
content = data["choices"][0]["message"].get("content", "")
if isinstance(content, list):
content = " ".join(
part.get("text", "") for part in content if isinstance(part, dict)
)
content = (content or "").strip()
if not content:
print("FAIL: image response content is empty", file=sys.stderr)
sys.exit(1)
lower = content.lower()
if "红" not in lower and "red" not in lower:
print(f"FAIL: expected red, got: {content[:200]}", file=sys.stderr)
sys.exit(1)
print(f"PASS: image: {content[:200]}")
PY
fi
fi
echo
echo "All checks passed for ${KIND} at ${BASE_URL}."