Files
gemma4-bi150/README.md
xiejiahao f8aabd5717 Honour vllm flags appended after the image, highest priority
Previously any arguments after the image in docker run were silently
dropped: entrypoint.sh never forwarded "$@", so
  docker run ... <image> --served-model-name mymodel
started fine but kept serving as llm with no warning.

- entrypoint.sh: forward "$@"; skip script-generated flags the user
  already provided so vllm never sees duplicate keys; strip those keys
  from EXTRA_ARGS when the command line also sets them
- precedence: command line > EXTRA_ARGS > same-name -e > detected default
- warn when --port / --served-model-name is overridden, since the host
  -p side and the client model field must change too
- README: document the three ways and their precedence
2026-10-28 14:46:32 +08:00

16 KiB
Raw Permalink Blame History

gemma4-bi150 — Gemma 4 家族统一部署(Iluvatar BI-V150)

一个镜像同时支持两个 Gemma 4 家族模型,启动时读模型 config.json 自动选择运行参数:

模型 类型 权重 TP 卡数 eager 显存 seqs
gemma-4-31B-it dense,60 层 58.25 GiB 4 4 0 0.90 4
GEV-26B-Decide MoE,30 层 / 128 experts 48.07 GiB 2 2 1 0.95 2

两个变体共用:容器内端口 80、模型名 llm、宿主端口 24396(由 -p 决定)。

本仓库由原先各自独立的 gemma4-31b-deploy 和 gev-26b-decide-deploy 两套目录合并而来, 合并依据见文末「为什么可以合并」。


1. 目录结构

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. 快速开始

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 不传模型目录

./run.sh

按顺序尝试:

/mnt/disk0/models/google/gemma-4-31B-it
/mnt/disk0/models/autotrust/GEV-26B-Decide

2.2 Compose

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 判断变体

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 / eager1 / 0.95 / 2 seqs
experts 缺失或 == 1 Gemma-4-31B(dense) TP4 / 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 反查:

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 跑到一半才崩:

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 检测输出

$ 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   # 仅提示,模型名固定 llm
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)'
python3 detect_model.py <dir> --json          # verify.sh 用
python3 detect_model.py <dir> --tp 4          # 按 TP4 校验可行性

4. 覆盖启动参数

显式环境变量优先于检测结果。

# 换卡
GPUS=12,13,14,15 ./run.sh /mnt/disk0/models/google/gemma-4-31B-it

# 换端口和模型名
HOST_PORT=24397 ./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

常用环境变量:

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
eager 0 1
显存 0.90 0.95
seqs 4 2

写进镜像就只能二选一,或者做两个镜像,违背"一个家族一个镜像"的目标。 所以启动方式统一由 run.sh / docker-compose.yml 传入:

docker run ... \
  --entrypoint /usr/local/bin/gemma4-entrypoint.sh \
  --workdir /opt/gemma4 \
  -p 24396:80 \
  -e MODEL_PATH=/models/gemma4 \
  -e PORT=80 \
  -e SERVED_NAME=llm \
  -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 通常用不到,但保持幂等一起打,避免同一镜像换模型后失效。

补丁是针对这个精确基础镜像生成的,换镜像需要重新生成:

registry.iluvatar.com.cn:10443/customer/sz/vllm0.17.0-4.4.0-x86:v7
vLLM 0.17.0+corex.20260612070235

7. 验证

# 自动找运行中的容器
./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:24396 --served-name llm

检查项:/health、/v1/models、/v1/chat/completions,可选推理题和图片识别。 只依赖 bash、curl、python3。全部通过返回 0。

/health 只看 curl 退出码,不看 body——vLLM 会返回 200 但 body 为空。


8. 关键参数的理由

8.1 Gemma-4-31B 为什么是 TP4

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

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:

Gemma-4-31B  → 15,14,13,12
GEV-26B-Decide → 14,15

卡数不足会直接报错,卡数多余只警告(vLLM 只用前 TP 张):

GPUS=12,13 ./run.sh .../gemma-4-31B-it        # 报错:需要 4 张
GPUS=0,1,2,3,4,5 ./run.sh .../GEV-26B-Decide  # 警告:只用其中 2 张

查看空闲卡:

/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 与发布

# 首次
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
harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0

拉取(需要 Harbor 凭据 DOCKER_USERNAME / DOCKER_PASSWORD):

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 卡,宿主 24396 → 容器 80)

docker run -d --name gemma4-31b \
  --entrypoint /usr/local/bin/gemma4-entrypoint.sh \
  --workdir /opt/gemma4 \
  --privileged --pid host --ipc host \
  -v /dev:/dev \
  -v /mnt/disk0/models/google/gemma-4-31B-it:/models/gemma4:ro \
  --ulimit memlock=-1 --ulimit stack=67108864 \
  -p 24396:80 \
  -e CUDA_VISIBLE_DEVICES=15,14,13,12 \
  -e MODEL_PATH=/models/gemma4 \
  -e PORT=80 \
  -e SERVED_NAME=llm \
  -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 卡,宿主 24396 → 容器 80)

docker run -d --name gev-26b-decide \
  --entrypoint /usr/local/bin/gemma4-entrypoint.sh \
  --workdir /opt/gemma4 \
  --privileged --pid host --ipc host \
  -v /dev:/dev \
  -v /mnt/disk0/models/autotrust/GEV-26B-Decide:/models/gemma4:ro \
  --ulimit memlock=-1 --ulimit stack=67108864 \
  -p 24396:80 \
  -e CUDA_VISIBLE_DEVICES=14,15 \
  -e MODEL_PATH=/models/gemma4 \
  -e PORT=80 \
  -e SERVED_NAME=llm \
  -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 节)。

容器内端口恒为 80、模型名恒为 llm,所以两个变体可以复用同一个宿主端口 24396,串行切换时客户端不用改地址也不用改 model 字段。

覆盖参数:三种写法,优先级从高到低

优先级 写法 例子
1 在镜像后面直接跟 vllm 参数 ... gemma4-bi150:v0.1.0 --served-model-name mymodel
2 -e EXTRA_ARGS="..." -e EXTRA_ARGS="--max-model-len 16384"
3 同名 -e -e MAX_MODEL_LEN=16384 -e SERVED_NAME=mymodel
4 默认值 / config.json 检测 什么都不传

三种写法都能生效,不会重复传参(脚本会跳过自己已经不需要传的项)。 注意覆盖 --port 或 --served-model-name 时会打印 WARN,因为宿主机 -p 的右侧端口和客户端 model 字段也要同步改。

# 写法 1:镜像后直接跟参数(最符合 vllm 习惯)
docker run -d --name gemma4-31b \
  --entrypoint /usr/local/bin/gemma4-entrypoint.sh --workdir /opt/gemma4 \
  --privileged --pid host --ipc host -v /dev:/dev \
  -v /mnt/disk0/models/google/gemma-4-31B-it:/models/gemma4:ro \
  --ulimit memlock=-1 --ulimit stack=67108864 \
  -p 24396:80 \
  -e CUDA_VISIBLE_DEVICES=15,14,13,12 -e MODEL_PATH=/models/gemma4 -e AUTO_DETECT=1 \
  harbor.4pd.io/modelhubxc/xiejiahao/gemma4-bi150:v0.1.0 \
  --served-model-name mymodel --max-model-len 16384

注意:并行跑两个变体时必须给不同的 -p 左侧端口,例如 -p 24397:80。

11.3 用 run.sh 启动 CI 镜像

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 可判
eager 0 1 ✅ config 可判
显存 / seqs 0.90 / 4 0.95 / 2 ✅ config 可判
端口 80 80 统一,无需判定
served name llm llm 统一,无需判定

差异项全部可由 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)