Files
gemma4-bi150/README.md

464 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# gemma4-bi150 — Gemma 4 家族统一部署(Iluvatar BI-V150)
一个镜像同时支持两个 Gemma 4 家族模型,启动时读模型 `config.json` 自动选择运行参数:
| 模型 | 类型 | 权重 | TP | 卡数 | 端口 | served name | eager | 显存 | seqs |
|---|---|---:|---:|---:|---:|---|---:|---:|---:|
| `gemma-4-31B-it` | dense,60 层 | 58.25 GiB | **4** | 4 | **8095** | `gemma-4-31B-it` | 0 | 0.90 | 4 |
| `GEV-26B-Decide` | MoE,30 层 / 128 experts | 48.07 GiB | **2** | 2 | **8094** | `GEV-26B-Decide` | 1 | 0.95 | 2 |
> 本仓库由原先各自独立的 `gemma4-31b-deploy` 和 `gev-26b-decide-deploy` 两套目录合并而来,
> 合并依据见文末「为什么可以合并」。
---
## 1. 目录结构
```text
gemma4-bi150-deploy/
├── Dockerfile # 只放基础镜像 + 补丁 + 脚本,不写 ENV/EXPOSE/ENTRYPOINT
├── entrypoint.sh # 容器入口:打补丁 → detect → exec vllm
├── detect_model.py # 从 config.json 判断 dense / MoE,并校验 TP 可行性
├── apply_patch.py # 幂等应用两个 CoreX vLLM 补丁
├── run.sh # 构建 + 启动 + 健康检查
├── verify.sh # health / models / chat / 可选推理 / 可选图片
├── docker-compose.yml
├── .dockerignore
├── .gitignore
├── .gitea/workflows/
│ ├── docker-build-push.yml
│ └── task_info.env
└── README.md
```
---
## 2. 快速开始
```bash
cd gemma4-bi150-deploy
# Gemma-4-31B(4 卡,默认用 15,14,13,12)
./run.sh /mnt/disk0/models/google/gemma-4-31B-it
# GEV-26B-Decide(2 卡,默认用 14,15)
./run.sh /mnt/disk0/models/autotrust/GEV-26B-Decide
# 验证
./verify.sh --model-dir /mnt/disk0/models/google/gemma-4-31B-it
./verify.sh --model-dir /mnt/disk0/models/autotrust/GEV-26B-Decide --with-image
```
模型类型不需要手写参数,`run.sh` 会调用 `detect_model.py` 读 `config.json` 判断。
### 2.1 不传模型目录
```bash
./run.sh
```
按顺序尝试:
```text
/mnt/disk0/models/google/gemma-4-31B-it
/mnt/disk0/models/autotrust/GEV-26B-Decide
```
### 2.2 Compose
```bash
MODEL_HOST_DIR=/mnt/disk0/models/google/gemma-4-31B-it docker compose up -d --build
```
Compose 里 TP / PORT / SERVED_NAME / MAX_NUM_SEQS / GPU_MEM_UTIL / ENFORCE_EAGER
都默认留空,由容器内 `entrypoint.sh` 检测后填充。
---
## 3. 自动识别逻辑
### 3.1 判断变体
```python
arch = config["architectures"][0] # 必须以 Gemma4 开头
text = config.get("text_config", config)
experts = num_experts / num_local_experts / n_routed_experts
```
| 条件 | 变体 | 运行参数 |
|---|---|---|
| `experts > 1` | GEV-26B-Decide(MoE) | TP2 / 8094 / eager1 / 0.95 / 2 seqs |
| `experts` 缺失或 `== 1` | Gemma-4-31B(dense) | TP4 / 8095 / eager0 / 0.90 / 4 seqs |
| 无 experts 且目录名含 `gev` | GEV | 同上 |
| 无 experts 且目录名含 `gemma` | Gemma-4-31B | 同上 |
| 其它 / 架构不是 Gemma4 | 直接报错 | 不启动 |
`num_experts=1` 按 dense 处理,因为部分 dense 配置用 1 表示"没有专家"。
### 3.2 顺便校验 TP 可行性(这是合并后的额外收益)
两个变体都有硬性 TP 上限,`detect_model.py` 会拿 `config.json` 反查:
```text
dense: num_global_key_value_heads % TP == 0
Gemma-4-31B 的 4 个 global KV head,TP8 会切成 0.5 → 拒绝启动
MoE: (moe_intermediate_size / TP) % 32 == 0
CoreX ixfops.moe_w16a16_group_gemm 要求 k % 32 == 0
GEV 704/2=352 ✅,704/4=176 ❌
```
所以下面这些都会被提前拦住,而不是等 vLLM 跑到一半才崩:
```bash
TP=8 ./run.sh .../gemma-4-31B-it # num_global_key_value_heads=4 % 8 != 0
TP=4 ./run.sh .../GEV-26B-Decide # 704/4=176,k % 32 != 0
```
确实需要强跑时用 `GEMMA4_SKIP_TP_CHECK=1` 降级为警告。
### 3.3 检测输出
```text
$ python3 detect_model.py /mnt/disk0/models/autotrust/GEV-26B-Decide
DETECTED_MODEL_KIND=gev-26b-decide
DETECTED_TP=2
DETECTED_PORT=8094
DETECTED_SERVED_NAME=GEV-26B-Decide
DETECTED_GPU_COUNT=2
DETECTED_MAX_NUM_SEQS=2
DETECTED_GPU_MEM_UTIL=0.95
DETECTED_ENFORCE_EAGER=1
DETECTED_DESCRIPTION='AutoTrust GEV-26B-Decide MoE (BF16, TP2, 2 GPUs)'
```
```bash
python3 detect_model.py <dir> --json # verify.sh 用
python3 detect_model.py <dir> --tp 4 # 按 TP4 校验可行性
```
---
## 4. 覆盖启动参数
显式环境变量优先于检测结果。
```bash
# 换卡
GPUS=12,13,14,15 ./run.sh /mnt/disk0/models/google/gemma-4-31B-it
# 换端口和模型名
PORT=9001 SERVED_NAME=my-gemma4 ./run.sh /mnt/disk0/models/google/gemma-4-31B-it
# 加 vLLM 参数
EXTRA_ARGS="--max-model-len 16384" ./run.sh /mnt/disk0/models/google/gemma-4-31B-it
# 加 docker 参数
EXTRA_DOCKER_ARGS="-v /mnt/disk1:/data:ro" ./run.sh /mnt/disk0/models/google/gemma-4-31B-it
# 用已有镜像(CI 产物)不重新 build
BUILD=0 IMAGE=harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0 \
./run.sh /mnt/disk0/models/google/gemma-4-31B-it
# 只打印命令
DRY_RUN=1 ./run.sh /mnt/disk0/models/google/gemma-4-31B-it
```
常用环境变量:
```text
IMAGE NAME MODEL_HOST_DIR MODEL_MOUNT PORT SERVED_NAME TP GPUS
MAX_MODEL_LEN MAX_NUM_SEQS GPU_MEM_UTIL MAX_IMAGES ENFORCE_EAGER
AUTO_DETECT HEALTH_TIMEOUT EXTRA_ARGS EXTRA_DOCKER_ARGS BUILD DRY_RUN
GEMMA4_SKIP_TP_CHECK
```
`AUTO_DETECT=0` 可关闭容器内检测,全部手动给值。
---
## 5. 为什么 Dockerfile 里没有 ENV / EXPOSE / ENTRYPOINT
镜像必须能同时服务 dense 和 MoE 两个变体,而这些参数在两个变体之间不同:
| 参数 | Gemma-4-31B | GEV-26B-Decide |
|---|---:|---:|
| TP | 4 | 2 |
| 端口 | 8095 | 8094 |
| eager | 0 | 1 |
| 显存 | 0.90 | 0.95 |
| seqs | 4 | 2 |
写进镜像就只能二选一,或者做两个镜像,违背"一个家族一个镜像"的目标。
所以启动方式统一由 `run.sh` / `docker-compose.yml` 传入:
```bash
docker run ... \
--entrypoint /usr/local/bin/gemma4-entrypoint.sh \
--workdir /opt/gemma4 \
--expose 8095 \
-e MODEL_PATH=/models/gemma4 \
-e AUTO_DETECT=1 \
<image>
```
---
## 6. 补丁
`apply_patch.py` 应用两个补丁,构建镜像时和容器启动时都会幂等执行一次。
| 补丁 | 目标文件 | 不打的后果 |
|---|---|---|
| `MultiModalDataDict` 导入路径 | `vllm/model_executor/models/gemma4_mm.py` | `ImportError`,模型检查阶段就失败(**两个变体都需要**) |
| decode fast path 的 attention metadata 判断 | `vllm/model_executor/layers/fused_moe/fused_moe.py` | GEV 的 CUDA graph capture 报 `AttributeError: no attribute 'num_decodes'` |
第二个补丁对 dense 的 Gemma-4-31B 通常用不到,但保持幂等一起打,避免同一镜像换模型后失效。
补丁是针对这个精确基础镜像生成的,换镜像需要重新生成:
```text
registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v7
vLLM 0.17.0+corex.20260612070235
```
---
## 7. 验证
```bash
# 自动找运行中的容器
./verify.sh
# 指定模型目录(推荐,能顺带校验变体)
./verify.sh --model-dir /mnt/disk0/models/google/gemma-4-31B-it
# 指定容器
./verify.sh --container gemma4-31b
# 额外图片识别
./verify.sh --model-dir /mnt/disk0/models/autotrust/GEV-26B-Decide --with-image
# 额外推理题(期望 27)
./verify.sh --model-dir /mnt/disk0/models/google/gemma-4-31B-it --with-reasoning
# 直接指定服务地址
./verify.sh --base-url http://127.0.0.1:8095 --served-name gemma-4-31B-it
```
检查项:`/health`、`/v1/models`、`/v1/chat/completions`,可选推理题和图片识别。
只依赖 `bash`、`curl`、`python3`。全部通过返回 0。
> `/health` 只看 `curl` 退出码,不看 body——vLLM 会返回 200 但 body 为空。
---
## 8. 关键参数的理由
### 8.1 Gemma-4-31B 为什么是 TP4
```text
Gemma4ForConditionalGeneration
num_attention_heads = 32
num_key_value_heads = 16 # sliding attention
num_global_key_value_heads = 4 # full attention
head_dim / global_head_dim = 256 / 512
num_hidden_layers = 60
```
`num_global_key_value_heads=4` 是最紧的约束:TP8 会切成 0.5,当前 vLLM 不支持。
TP4 下 4/4=1,权重 58.25 GiB / 4 ≈ 14.6 GiB/卡,实测加载后 15.89 GiB/卡。
### 8.2 GEV-26B-Decide 为什么是 TP2
```text
Gemma4ForConditionalGeneration
hidden_size = 2816
num_hidden_layers = 30
num_attention_heads = 16
num_key_value_heads = 8
moe_intermediate_size = 704
num_experts = 128
top_k_experts = 8
```
CoreX `ixfops.moe_w16a16_group_gemm` 要求分组 GEMM 的 K 维能被 32 整除,
`704 / TP` 的结果:TP8→88 ❌、TP4→176 ❌、TP2→352 ✅。
TP1 显存不够,所以只有 TP2。
### 8.3 GEV 为什么开 `--enforce-eager`
MoE + CoreX 的 CUDA graph 路径不稳定,实测开 eager 才能稳定启动;
dense 的 Gemma-4-31B 不需要,关掉可以拿到更好的吞吐。
### 8.4 GEV 为什么用 0.95 显存
权重 48.07 GiB / 2 ≈ 24.99 GiB/卡,0.95 下 KV 约 4.9 GiB,适合低并发。
`max-num-seqs` 同时降到 2,避免 KV 被并发打爆。
---
## 9. GPU 选择
`run.sh` 默认优先序号大的卡,避免和 16 卡大模型抢 0-11:
```text
Gemma-4-31B → 15,14,13,12
GEV-26B-Decide → 14,15
```
卡数不足会直接报错,卡数多余只警告(vLLM 只用前 TP 张):
```bash
GPUS=12,13 ./run.sh .../gemma-4-31B-it # 报错:需要 4 张
GPUS=0,1,2,3,4,5 ./run.sh .../GEV-26B-Decide # 警告:只用其中 2 张
```
查看空闲卡:
```bash
/usr/local/corex/bin/ixsmi
```
---
## 10. 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| `ImportError: cannot import name 'MultiModalDataDict'` | 补丁没打上 | 确认 `apply_patch.py` 执行过 |
| `AttributeError: 'TritonAttentionMetadata' object has no attribute 'num_decodes'` | MoE 补丁没打上 | 同上 |
| `Expected k % 32 == 0 to be true` | GEV 用了 TP4/TP8 | 回到 TP2;`detect_model.py` 会提前拦住 |
| KV head 无法切分 / head 数不整除 | dense 用了 TP8 | 回到 TP4 |
| `cudaGetDeviceCount() ... Error 1001` | 少了 `--privileged -v /dev:/dev` | 用 `run.sh`,不要手写简化版 `docker run` |
| 启动 10 分钟还没 ready | 冷读 49–59 GB 权重 | 默认给 900s;`docker logs -f` 看 `Loading weights` |
| `/health` 超时但容器在跑 | 还在加载权重 | 以 `Loading weights took` 和 `/v1/models` 为准 |
---
## 11. CI 与发布
```bash
# 首次
git tag v0.1.0 && git push origin v0.1.0
```
已发布:`v0.1.0`(CI run #1 成功)。
- 只有推送 `v*` 标签才触发;
- runner label:`amd64-ubuntu-24.04`,需要 `DOCKER_*` / `FIXED_TOKEN`;
- `task_info.env`:`FRAMEWORK=vllm_gemma_4`(必须独特,不要用泛化的 `vllm`);
- CI 被取消后**不要复用标签**,直接 bump patch;
- CI 构建 vLLM 基础镜像较久,首次推送约 1.5 小时量级;后续改动若层缓存命中会快很多(本次 71 秒)。
### 11.1 已发布的 CI 镜像
| 标签 | Digest | 压缩大小 | 推送时间 |
|---|---|---:|---|
| `v0.1.0` | `sha256:0f6eb25cc2a536529368ac32e0b622f5e4da84c90d63ccf036752c587e45c6c9` | 10.39 GiB | 2026-10-27 06:01 UTC |
```text
harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0
```
拉取(需要 Harbor 凭据 `DOCKER_USERNAME` / `DOCKER_PASSWORD`):
```bash
docker login harbor.4pd.io -u modelhubxc
docker pull harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0
```
CI 的 `naming to` 行与本表 digest 一致,可作为复核依据。
### 11.2 用 CI 镜像启动服务
镜像不声明 `ENTRYPOINT`,`--entrypoint` 必须显式给。以下命令可直接粘贴。
**Gemma-4-31B-it(TP4,4 卡,8095)**
```bash
docker run -d --name gemma4-31b \
--entrypoint /usr/local/bin/gemma4-entrypoint.sh \
--workdir /opt/gemma4 \
--privileged --pid host --network host --ipc host \
-v /dev:/dev \
-v /mnt/disk0/models/google/gemma-4-31B-it:/models/gemma4:ro \
--ulimit memlock=-1 --ulimit stack=67108864 \
-e CUDA_VISIBLE_DEVICES=15,14,13,12 \
-e MODEL_PATH=/models/gemma4 \
-e AUTO_DETECT=1 \
-e PYTHONUNBUFFERED=1 \
--restart unless-stopped \
harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0
```
**GEV-26B-Decide(TP2,2 卡,8094)**
```bash
docker run -d --name gev-26b-decide \
--entrypoint /usr/local/bin/gemma4-entrypoint.sh \
--workdir /opt/gemma4 \
--privileged --pid host --network host --ipc host \
-v /dev:/dev \
-v /mnt/disk0/models/autotrust/GEV-26B-Decide:/models/gemma4:ro \
--ulimit memlock=-1 --ulimit stack=67108864 \
-e CUDA_VISIBLE_DEVICES=14,15 \
-e MODEL_PATH=/models/gemma4 \
-e AUTO_DETECT=1 \
-e PYTHONUNBUFFERED=1 \
--restart unless-stopped \
harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0
```
两个变体共用挂载点 `/models/gemma4`,靠 `config.json` 区分;TP / 端口 / eager / 显存 / 并发数都不用传,入口脚本会自动判定(见第 3 节)。
需要覆盖时加同名 `-e`,例如 `-e MAX_MODEL_LEN=16384`、`-e PORT=24396`。
**注意**:`--network host` 下不要写 `-p`,端口映射会被丢弃;要换对外端口就传 `-e PORT=<port>`。
### 11.3 用 run.sh 启动 CI 镜像
```bash
BUILD=0 IMAGE=harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0 \
./run.sh /mnt/disk0/models/google/gemma-4-31B-it
./verify.sh --model-dir /mnt/disk0/models/google/gemma-4-31B-it
```
---
## 12. 为什么可以合并
合并前是 `gemma4-31b-deploy/` 和 `gev-26b-decide-deploy/` 两套目录。逐项对比:
| 项 | Gemma-4-31B | GEV-26B-Decide | 能否 detect |
|---|---|---|---|
| 基础镜像 | 同一个 | 同一个 | — |
| 架构 | `Gemma4ForConditionalGeneration` | 同一个 | 不需要 |
| vLLM 补丁 | 两个 | 同样两个 | 不需要 |
| docker run 参数 | 相同 | 相同 | — |
| TP | 4 | 2 | ✅ config 可判 |
| 端口 | 8095 | 8094 | ✅ config 可判 |
| served name | `gemma-4-31B-it` | `GEV-26B-Decide` | ✅ config 可判 |
| eager | 0 | 1 | ✅ config 可判 |
| 显存 / seqs | 0.90 / 4 | 0.95 / 2 | ✅ config 可判 |
差异项全部可由 `config.json` 推断,和 Llama 4 用 `num_local_experts` 分 Scout/Maverick 是同一形态,
因此按"一个模型家族一个仓库"的原则合并。
顺带解决的问题:
1. 两份 `apply_patch.py` 原本只差注释,现在只有一份;
2. 两个 Dockerfile 原本都写死了 `ENV/EXPOSE/ENTRYPOINT`,现在通用;
3. 一次 CI 覆盖两个模型,而不是两次构建(每次约 1.5 小时);
4. 新增 TP 可行性校验,把"GEV 只能 TP2、Gemma4 只能 TP4"从文档结论变成启动前断言。
---
## 13. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-27 | 由 `gemma4-31b-deploy` + `gev-26b-decide-deploy` 合并为本目录;新增 `detect_model.py`、`verify.sh`、`.gitea`;Dockerfile 去掉 ENV/EXPOSE/ENTRYPOINT;新增 TP 可行性校验和 `GEMMA4_SKIP_TP_CHECK` |
| 2026-10-27 | CI `v0.1.0` 发布成功(digest `0f6eb25c…`,10.39 GiB) |
| 2026-10-28 | 补录 CI 镜像 digest 与 Gemma-4-31B / GEV 的完整 `docker run` 命令(§11.1–11.3) |