commit 8022cd4b2e486fec564446d1e260f38f9fcd20ae Author: 4paradigm <4paradigm@4paradigmdeMacBook-Pro.local> Date: Tue Jul 21 10:18:53 2026 +0800 fix tokenizer diff --git a/.gitea/workflows/docker-build-push.yml b/.gitea/workflows/docker-build-push.yml new file mode 100644 index 0000000..9c5c285 --- /dev/null +++ b/.gitea/workflows/docker-build-push.yml @@ -0,0 +1,132 @@ +name: Docker Build and Push + +on: + push: + tags: + - "v*" + +jobs: + docker: + runs-on: amd64-ubuntu-24.04 + + steps: + - name: Clone repository + run: | + git clone "${{ gitea.server_url }}/${{ gitea.repository }}.git" . + git checkout "${{ gitea.ref_name }}" + + - name: Set image metadata + run: | + IMAGE_NAME="$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]' | tr '_' '-')" + IMAGE="${DOCKER_REGISTRY}/${DOCKER_USERNAME}/${IMAGE_NAME}:${{ gitea.ref_name }}" + + echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITEA_ENV" + echo "IMAGE=${IMAGE}" >> "$GITEA_ENV" + + - name: Load and Validate Task Info + run: | + set -a + . .gitea/workflows/task_info.env + set +a + + for name in FRAMEWORK GPU_TYPE TASK_TYPE; do + eval "value=\${${name}:-}" + if [ "$name" = "FRAMEWORK" ] && [ -z "$value" ]; then + echo "${name} is empty in .gitea/workflows/task_info.env" + exit 1 + fi + + echo "${name}=${value}" >> "$GITEA_ENV" + done + + - name: Validate Image Verify Metadata + run: | + if [ -z "${FIXED_TOKEN:-}" ]; then + echo "FIXED_TOKEN is not configured on runner" + exit 1 + fi + + if ! response="$(curl --silent --show-error --location --get 'https://modelhub.org.cn/adminApi/image-verify/validate' \ + --header "Xc-Token: ${FIXED_TOKEN}" \ + --data-urlencode "gpuType=${GPU_TYPE:-}" \ + --data-urlencode "taskType=${TASK_TYPE:-}")"; then + echo "failed to call image verify validate API" + exit 1 + fi + + VALIDATE_RESPONSE="$response" python3 - <<'PY' + import json + import os + import sys + + raw = os.environ.get("VALIDATE_RESPONSE", "") + try: + body = json.loads(raw) + except json.JSONDecodeError: + print("image verify validate API returned invalid JSON") + print(raw) + sys.exit(1) + + if body.get("code") == 0 and body.get("data") is True: + print("image verify metadata validation passed") + sys.exit(0) + + message = body.get("message") or "unknown error" + print(f"image verify metadata validation failed: {message}") + print(raw) + sys.exit(1) + PY + + - name: Login to Docker Registry + run: | + echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" \ + -u "$DOCKER_USERNAME" \ + --password-stdin + + - name: Build Docker Image + run: | + docker build -t "$IMAGE" . + + - name: Push Docker Image + run: | + for attempt in 1 2 3; do + echo "Starting docker push attempt ${attempt}/3 for ${IMAGE}" + docker push "$IMAGE" & + PUSH_PID=$! + + while kill -0 "$PUSH_PID" 2>/dev/null; do + echo "docker push is still running at $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + sleep 60 + done + + if wait "$PUSH_PID"; then + echo "docker push completed successfully" + exit 0 + fi + + echo "docker push failed on attempt ${attempt}/3" + sleep 30 + done + + echo "docker push failed after 3 attempts" + exit 1 + + - name: Notify Image Verify + run: | + if [ -z "${FIXED_TOKEN:-}" ]; then + echo "FIXED_TOKEN is not configured on runner" + exit 1 + fi + + curl --silent --show-error --fail-with-body --location --request POST 'https://modelhub.org.cn//adminApi/image-verify' \ + --header "Xc-Token: ${FIXED_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data-raw "{ + \"framework\": \"${FRAMEWORK}\", + \"gpuType\": \"${GPU_TYPE}\", + \"imageUrl\": \"${IMAGE}\", + \"taskType\": \"${TASK_TYPE}\", + \"createBy\": \"${{ gitea.actor }}\", + \"repoUrl\": \"${{ gitea.server_url }}/${{ gitea.repository }}\", + \"tag\": \"${{ github.ref_name }}\" + }" diff --git a/.gitea/workflows/task_info.env b/.gitea/workflows/task_info.env new file mode 100644 index 0000000..aeee76c --- /dev/null +++ b/.gitea/workflows/task_info.env @@ -0,0 +1,3 @@ +FRAMEWORK=vllm_tokenizer_patch +GPU_TYPE=Iluvatar_bi-150 +TASK_TYPE=text-generation diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..151df5f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM harbor-contest.4pd.io/luopingyi/enginex-iluvatar-bi150/vllm:0.8.3 + +COPY fix_tokenizer.py /opt/fix_tokenizer.py +COPY vllm_wrapper.sh /opt/vllm_wrapper.sh + +RUN set -e; \ + vllm_bin="$(command -v vllm)"; \ + mv "$vllm_bin" "$(dirname "$vllm_bin")/vllm_real"; \ + cp /opt/vllm_wrapper.sh "$vllm_bin"; \ + chmod +x "$vllm_bin" /opt/fix_tokenizer.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..8054a03 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# BI-150 TokenizersBackend 修复补丁 + +基于 BI-150 vLLM 0.8.3 镜像,仅修复以下错误: + +```text +ValueError: Tokenizer class TokenizersBackend does not exist or is not currently imported. +``` + +## 工作方式 + +镜像构建时用 wrapper 接管 `vllm` 命令。执行 `vllm serve ` 时: + +1. 读取模型的 `tokenizer_config.json`。 +2. 仅当 `tokenizer_class` 严格等于 `TokenizersBackend` 时,将 tokenizer 文件复制到 `/tmp/fixed_tokenizer`。 +3. 根据已有文件选择替代类:`tokenizer.json` 使用 `PreTrainedTokenizerFast`,`tokenizer.model` 使用 `LlamaTokenizer`,`vocab.json` 与 `merges.txt` 使用 `GPT2TokenizerFast`;无法判断时回退到 `PreTrainedTokenizerFast`。 +4. 向原始 vLLM 命令追加 `--tokenizer /tmp/fixed_tokenizer`。 + +原模型目录不会被修改。其他 tokenizer 错误及其他 `tokenizer_class` 均不处理。 + +## 构建 + +```bash +docker build -t iluvatar-bi150-vllm-tokenizer-fix:0.8.3 . +``` + +使用时仅替换原 BI-150 镜像名,`vllm serve` 参数保持不变。 diff --git a/fix_tokenizer.py b/fix_tokenizer.py new file mode 100644 index 0000000..42538f5 --- /dev/null +++ b/fix_tokenizer.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +检测并修复 tokenizer_config.json 中的两类问题: +1. tokenizer_class 在 transformers 中不存在(如 TokenizersBackend) +2. extra_special_tokens 为 list 格式(transformers 要求 dict) + +若存在问题,将 tokenizer 文件复制到 /tmp/fixed_tokenizer/ 并修复, +最后将修复目录路径输出到 stdout。若无需修复,输出为空。 +""" +import os +import sys +import json +import shutil + +MODEL_DIR = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("MODEL_DIR", "/model") +OUT_DIR = "/tmp/fixed_tokenizer" + + +def main(): + cfg_path = os.path.join(MODEL_DIR, "tokenizer_config.json") + if not os.path.exists(cfg_path): + return + + with open(cfg_path) as f: + cfg = json.load(f) + + fixes = [] + + # --- 检测 1:tokenizer_class 是否在 transformers 中存在 --- + tokenizer_class = cfg.get("tokenizer_class", "") + bad_tokenizer_class = False + if tokenizer_class: + import transformers + if getattr(transformers, tokenizer_class, None) is None: + bad_tokenizer_class = True + fixes.append(f"tokenizer_class '{tokenizer_class}' not found in transformers") + + # --- 检测 2:extra_special_tokens 是否为 list 格式 --- + bad_extra_special_tokens = ( + "extra_special_tokens" in cfg + and isinstance(cfg["extra_special_tokens"], list) + ) + if bad_extra_special_tokens: + fixes.append("extra_special_tokens is a list, expected dict") + + if not fixes: + return # 无需修复 + + # 复制 tokenizer 文件到临时目录 + os.makedirs(OUT_DIR, exist_ok=True) + for fname in [ + "tokenizer.json", + "tokenizer_config.json", + "special_tokens_map.json", + "vocab.json", + "merges.txt", + "tokenizer.model", + ]: + src = os.path.join(MODEL_DIR, fname) + if os.path.exists(src): + shutil.copy(src, OUT_DIR) + + # --- 修复 1:替换 tokenizer_class --- + if bad_tokenizer_class: + files = os.listdir(MODEL_DIR) + if "tokenizer.json" in files: + fixed_class = "PreTrainedTokenizerFast" + elif "tokenizer.model" in files: + fixed_class = "LlamaTokenizer" + elif "vocab.json" in files and "merges.txt" in files: + fixed_class = "GPT2TokenizerFast" + else: + fixed_class = "PreTrainedTokenizerFast" + cfg["tokenizer_class"] = fixed_class + print( + f"[fix_tokenizer] tokenizer_class: '{tokenizer_class}' → '{fixed_class}'", + file=sys.stderr, + ) + + # --- 修复 2:extra_special_tokens list → dict --- + if bad_extra_special_tokens: + orig_list = cfg["extra_special_tokens"] + cfg["extra_special_tokens"] = {token: token for token in orig_list} + print( + f"[fix_tokenizer] extra_special_tokens: list({len(orig_list)}) → dict", + file=sys.stderr, + ) + + with open(os.path.join(OUT_DIR, "tokenizer_config.json"), "w") as f: + json.dump(cfg, f, indent=2) + + print(OUT_DIR) # 输出修复目录,供 entrypoint.sh 捕获 + + +main() diff --git a/vllm_wrapper.sh b/vllm_wrapper.sh new file mode 100644 index 0000000..1f7c884 --- /dev/null +++ b/vllm_wrapper.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +VLLM_REAL="$(dirname "$(readlink -f "$0")")/vllm_real" + +# 仅拦截 vllm serve ,其他命令原样透传。 +if [ "$1" = "serve" ] && [ -n "$2" ]; then + MODEL_DIR="$2" + shift 2 + + FIXED_DIR=$(python3 /opt/fix_tokenizer.py "$MODEL_DIR") + if [ -n "$FIXED_DIR" ]; then + exec "$VLLM_REAL" serve "$MODEL_DIR" --tokenizer "$FIXED_DIR" "$@" + fi + + exec "$VLLM_REAL" serve "$MODEL_DIR" "$@" +fi + +exec "$VLLM_REAL" "$@"