Add Qwen2.5 tokenizer compatibility engine and runner workflow
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.git
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
tests
|
||||||
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
|
*.py text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.env text eol=lf
|
||||||
|
Dockerfile text eol=lf
|
||||||
98
.gitea/workflows/docker-build-push.yml
Normal file
98
.gitea/workflows/docker-build-push.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: Build and verify Qwen2.5 compatibility engines
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: amd64-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Clone tagged source
|
||||||
|
run: |
|
||||||
|
git clone "${{ gitea.server_url }}/${{ gitea.repository }}.git" .
|
||||||
|
git checkout "${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Run compatibility tests
|
||||||
|
run: python3 -m unittest discover -s tests -q
|
||||||
|
|
||||||
|
- name: Load image metadata
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
set -a
|
||||||
|
. .gitea/workflows/task_info.env
|
||||||
|
set +a
|
||||||
|
test -n "$FRAMEWORK"
|
||||||
|
test -n "$TASK_TYPE"
|
||||||
|
test -n "$GPU_TYPES"
|
||||||
|
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: |
|
||||||
|
set -eu
|
||||||
|
test -n "${FIXED_TOKEN:-}" || { echo "FIXED_TOKEN is missing"; exit 1; }
|
||||||
|
for gpu in $GPU_TYPES; do
|
||||||
|
response="$(curl --silent --show-error --fail-with-body --location --get \
|
||||||
|
'https://modelhub.org.cn/adminApi/image-verify/validate' \
|
||||||
|
--header "Xc-Token: $FIXED_TOKEN" \
|
||||||
|
--data-urlencode "gpuType=$gpu" \
|
||||||
|
--data-urlencode "taskType=$TASK_TYPE")"
|
||||||
|
VALIDATE_RESPONSE="$response" python3 -c 'import json,os,sys; body=json.loads(os.environ["VALIDATE_RESPONSE"]); sys.exit(0 if body.get("code")==0 and body.get("data") is True else 1)'
|
||||||
|
echo "validated gpuType=$gpu taskType=$TASK_TYPE"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Log in to Docker registry
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
test -n "${DOCKER_REGISTRY:-}"
|
||||||
|
test -n "${DOCKER_USERNAME:-}"
|
||||||
|
test -n "${DOCKER_PASSWORD:-}"
|
||||||
|
printf '%s' "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" \
|
||||||
|
-u "$DOCKER_USERNAME" --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push target images
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
image_name="$(printf '%s' '${{ gitea.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"
|
||||||
|
|
||||||
|
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" .
|
||||||
|
|
||||||
|
for image in "$a100_image" "$cambricon_image"; do
|
||||||
|
success=0
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if docker push "$image"; then success=1; break; fi
|
||||||
|
echo "docker push attempt $attempt failed for $image"
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
test "$success" -eq 1
|
||||||
|
done
|
||||||
|
echo "A100_IMAGE=$a100_image" >> "$GITEA_ENV"
|
||||||
|
echo "CAMBRICON_IMAGE=$cambricon_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" ;;
|
||||||
|
*) echo "unknown GPU target: $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"]}))')"
|
||||||
|
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 "$payload"
|
||||||
|
done
|
||||||
3
.gitea/workflows/task_info.env
Normal file
3
.gitea/workflows/task_info.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
FRAMEWORK=vllm_qwen25_compat
|
||||||
|
TASK_TYPE=text-generation
|
||||||
|
GPU_TYPES="Nvidia-A100 Cambricon_mlu-370-x4 Cambricon_mlu-370-x8"
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.env
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
ARG BASE_IMAGE=harbor.4pd.io/dooke/vllm/vllm/vllm-openai:v0.11.0
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
|
COPY engine_compat.py /opt/modelhub/engine_compat.py
|
||||||
|
COPY vllm_wrapper.sh /opt/modelhub/vllm_wrapper.sh
|
||||||
|
|
||||||
|
RUN set -eu; \
|
||||||
|
python3 -m py_compile /opt/modelhub/engine_compat.py; \
|
||||||
|
vllm_bin="$(command -v vllm)"; \
|
||||||
|
mv "$vllm_bin" "$(dirname "$vllm_bin")/vllm_real"; \
|
||||||
|
cp /opt/modelhub/vllm_wrapper.sh "$vllm_bin"; \
|
||||||
|
chmod 0755 "$vllm_bin" /opt/modelhub/engine_compat.py
|
||||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Qwen2.5 启动兼容引擎
|
||||||
|
|
||||||
|
这个独立镜像项目沿用 [ModelHub A100 示例仓库](https://dev.modelhub.org.cn/zhengzhongwei/vLLM-Nvidia-A100-opt)
|
||||||
|
的方式:标签触发 `.gitea/workflows/docker-build-push.yml`,runner 检查元数据、构建并推送镜像,
|
||||||
|
最后调用 ModelHub 的 `image-verify` 接口注册验证。
|
||||||
|
|
||||||
|
当前为 A100 参考端和寒武纪 MLU370 x4/x8 构建两个镜像,三个 GPU 共用
|
||||||
|
`vllm_qwen25_compat` 框架名。A100 基础镜像来自示例仓库;寒武纪基础镜像来自
|
||||||
|
平台现有 MLU370 验证任务使用的镜像。是否允许一个仓库为多个 GPU 注册同一框架,
|
||||||
|
需要以 runner 的 `validate` 和镜像验证结果为准。
|
||||||
|
|
||||||
|
## 修复范围
|
||||||
|
|
||||||
|
- 将列表型 `extra_special_tokens` 合并到 `additional_special_tokens`,解决
|
||||||
|
Transformers 调用 `.keys()` 时的崩溃。
|
||||||
|
- 对明确写成 `TokenizersBackend` 且带 `tokenizer.json` 的仓库,改用
|
||||||
|
`PreTrainedTokenizerFast`。
|
||||||
|
- 对 `model_type=qwen2`、`eos_token=<|im_end|>` 且缺少聊天模板的模型,补充
|
||||||
|
简单的 Qwen ChatML 模板,避免 `/v1/chat/completions` 返回 400。
|
||||||
|
|
||||||
|
启动时在 `/tmp` 建立新模型视图,只重写 `tokenizer_config.json`。权重、模型代码和
|
||||||
|
其他文件以符号链接指回只读 `/model`;源模型不变。没有命中上述问题时直接调用原始
|
||||||
|
`vllm`。修复记录写在新视图的 `.modelhub-compat-report.json`,同时输出到容器日志。
|
||||||
|
|
||||||
|
这不能修复缺失权重、尺寸不匹配、Ascend 上持续出现的 0 分输出或平台评测超时。
|
||||||
|
尤其是分数为 0 的参考端与目标端必须分别排查;只换一个端的镜像不能保证过审。
|
||||||
|
|
||||||
|
## 本地验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m unittest discover -s tests -v
|
||||||
|
python -m py_compile engine_compat.py
|
||||||
|
```
|
||||||
|
|
||||||
|
有对应镜像访问权限的 Docker 主机上再运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg BASE_IMAGE=harbor.4pd.io/dooke/vllm/vllm/vllm-openai:v0.11.0 \
|
||||||
|
-t qwen25-compat:a100 .
|
||||||
|
docker build --build-arg BASE_IMAGE=harbor.4pd.io/hardcore-tech/cambricon-mlu370-pytorch:v25.01-20260228 \
|
||||||
|
-t qwen25-compat:mlu370 .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 用 runner 发布
|
||||||
|
|
||||||
|
1. 在 `dev.modelhub.org.cn` 创建独立仓库,把本目录作为仓库根目录推送。
|
||||||
|
2. 为该仓库的 runner 配置 `DOCKER_REGISTRY`、`DOCKER_USERNAME`、
|
||||||
|
`DOCKER_PASSWORD`、`FIXED_TOKEN`。不要把凭证写进仓库。
|
||||||
|
3. 推送 `v*` 标签。工作流依次跑单元测试、验证 GPU/任务元数据、构建与推送镜像、
|
||||||
|
为 A100 和 MLU370 x4/x8 各发起一次镜像验证。
|
||||||
|
4. 在“我的驱动列表”确认三个镜像验证通过,并检查新框架出现在对应 GPU 的
|
||||||
|
`frameworks` 接口中,再用少量已知缺陷模型做对照验证。
|
||||||
|
|
||||||
|
只有平台确认新框架可选且 A100 参考端实际使用新镜像后,才应让 `xc-agent` 把
|
||||||
|
列表型 `extra_special_tokens` 的模型路由到此框架。当前智能体继续预检并跳过它们。
|
||||||
163
engine_compat.py
Normal file
163
engine_compat.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Prepare an isolated model view for known Qwen2.5 tokenizer defects.
|
||||||
|
|
||||||
|
The source model is never edited. Large model files are linked into /tmp.
|
||||||
|
Only tokenizer_config.json is rewritten when a supported repair is needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
QWEN_CHAT_TEMPLATE = (
|
||||||
|
"{% for message in messages %}"
|
||||||
|
"<|im_start|>{{ message['role'] }}\n"
|
||||||
|
"{{ message['content'] }}<|im_end|>\n"
|
||||||
|
"{% endfor %}"
|
||||||
|
"{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibilityError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _json_object(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
raise CompatibilityError(f"invalid JSON object: {path.name}") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise CompatibilityError(f"invalid JSON object: {path.name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _key(value: Any) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=False, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def repair_tokenizer_config(
|
||||||
|
model_dir: Path, tokenizer: dict[str, Any]
|
||||||
|
) -> tuple[dict[str, Any], list[str]]:
|
||||||
|
patched = dict(tokenizer)
|
||||||
|
repairs: list[str] = []
|
||||||
|
|
||||||
|
extra = patched.get("extra_special_tokens")
|
||||||
|
if isinstance(extra, list):
|
||||||
|
if any(
|
||||||
|
not (
|
||||||
|
isinstance(item, str)
|
||||||
|
or isinstance(item, dict) and isinstance(item.get("content"), str)
|
||||||
|
)
|
||||||
|
for item in extra
|
||||||
|
):
|
||||||
|
raise CompatibilityError("unsupported extra_special_tokens list")
|
||||||
|
additional = patched.get("additional_special_tokens", [])
|
||||||
|
if not isinstance(additional, list):
|
||||||
|
raise CompatibilityError("additional_special_tokens must be a list")
|
||||||
|
merged = list(additional)
|
||||||
|
seen = {_key(item) for item in merged}
|
||||||
|
for item in extra:
|
||||||
|
if _key(item) not in seen:
|
||||||
|
merged.append(item)
|
||||||
|
seen.add(_key(item))
|
||||||
|
patched["additional_special_tokens"] = merged
|
||||||
|
del patched["extra_special_tokens"]
|
||||||
|
repairs.append("extra_special_tokens list -> additional_special_tokens")
|
||||||
|
elif extra is not None and not isinstance(extra, dict):
|
||||||
|
raise CompatibilityError("extra_special_tokens must be a mapping or list")
|
||||||
|
|
||||||
|
# Only replace the known class mismatch from existing ModelHub failures.
|
||||||
|
# Arbitrary custom tokenizer classes may require their own Python code.
|
||||||
|
if patched.get("tokenizer_class") == "TokenizersBackend":
|
||||||
|
if not (model_dir / "tokenizer.json").is_file():
|
||||||
|
raise CompatibilityError("TokenizersBackend requires tokenizer.json")
|
||||||
|
patched["tokenizer_class"] = "PreTrainedTokenizerFast"
|
||||||
|
repairs.append("TokenizersBackend -> PreTrainedTokenizerFast")
|
||||||
|
|
||||||
|
if not patched.get("chat_template"):
|
||||||
|
config_path = model_dir / "config.json"
|
||||||
|
if config_path.is_file():
|
||||||
|
model_config = _json_object(config_path)
|
||||||
|
if (
|
||||||
|
model_config.get("model_type") == "qwen2"
|
||||||
|
and patched.get("eos_token") == "<|im_end|>"
|
||||||
|
):
|
||||||
|
patched["chat_template"] = QWEN_CHAT_TEMPLATE
|
||||||
|
repairs.append("added Qwen2 ChatML chat_template")
|
||||||
|
|
||||||
|
return patched, repairs
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_model(model_dir: str | Path, *, work_root: str | Path | None = None) -> str | None:
|
||||||
|
source = Path(model_dir).resolve()
|
||||||
|
if not source.is_dir():
|
||||||
|
raise CompatibilityError(f"model directory is missing: {source}")
|
||||||
|
config_path = source / "tokenizer_config.json"
|
||||||
|
if not config_path.is_file():
|
||||||
|
return None
|
||||||
|
|
||||||
|
patched, repairs = repair_tokenizer_config(source, _json_object(config_path))
|
||||||
|
if not repairs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
view = Path(tempfile.mkdtemp(prefix="modelhub-compat-", dir=work_root))
|
||||||
|
try:
|
||||||
|
for item in source.iterdir():
|
||||||
|
target = view / item.name
|
||||||
|
if item.name == "tokenizer_config.json":
|
||||||
|
target.write_text(
|
||||||
|
json.dumps(patched, ensure_ascii=False, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# The evaluation container is Linux; symlinks also work when
|
||||||
|
# /model is a read-only mount on a different filesystem.
|
||||||
|
try:
|
||||||
|
target.symlink_to(item.resolve(), target_is_directory=item.is_dir())
|
||||||
|
except OSError as exc:
|
||||||
|
if not item.is_file():
|
||||||
|
raise CompatibilityError(f"cannot link model item: {item.name}") from exc
|
||||||
|
try:
|
||||||
|
os.link(item, target)
|
||||||
|
except OSError as link_exc:
|
||||||
|
raise CompatibilityError(
|
||||||
|
f"cannot link model item: {item.name}"
|
||||||
|
) from link_exc
|
||||||
|
(view / ".modelhub-compat-report.json").write_text(
|
||||||
|
json.dumps({"source": str(source), "repairs": repairs}, ensure_ascii=False)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
for item in view.iterdir():
|
||||||
|
item.unlink()
|
||||||
|
view.rmdir()
|
||||||
|
raise
|
||||||
|
|
||||||
|
print(json.dumps({"model_view": str(view), "repairs": repairs}, ensure_ascii=False), file=sys.stderr)
|
||||||
|
return str(view)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("usage: engine_compat.py MODEL_DIR", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
try:
|
||||||
|
view = prepare_model(sys.argv[1])
|
||||||
|
except CompatibilityError as exc:
|
||||||
|
print(f"model compatibility error: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if view:
|
||||||
|
print(view)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
81
tests/test_engine_compat.py
Normal file
81
tests/test_engine_compat.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from engine_compat import QWEN_CHAT_TEMPLATE, CompatibilityError, prepare_model
|
||||||
|
|
||||||
|
|
||||||
|
class EngineCompatTests(unittest.TestCase):
|
||||||
|
def _model(self, root: Path, tokenizer: dict) -> Path:
|
||||||
|
model = root / "model"
|
||||||
|
model.mkdir()
|
||||||
|
(model / "config.json").write_text(
|
||||||
|
json.dumps({"model_type": "qwen2", "architectures": ["Qwen2ForCausalLM"]}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(model / "tokenizer_config.json").write_text(
|
||||||
|
json.dumps(tokenizer), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(model / "model.safetensors").write_bytes(b"sample-weights")
|
||||||
|
return model
|
||||||
|
|
||||||
|
def test_repairs_list_and_missing_qwen_chat_template(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source = self._model(
|
||||||
|
root,
|
||||||
|
{
|
||||||
|
"eos_token": "<|im_end|>",
|
||||||
|
"extra_special_tokens": ["<|im_start|>", "<|im_end|>"],
|
||||||
|
"additional_special_tokens": ["<|im_start|>"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
view = Path(prepare_model(source, work_root=root))
|
||||||
|
fixed = json.loads((view / "tokenizer_config.json").read_text("utf-8"))
|
||||||
|
original = json.loads((source / "tokenizer_config.json").read_text("utf-8"))
|
||||||
|
self.assertIn("extra_special_tokens", original)
|
||||||
|
self.assertNotIn("extra_special_tokens", fixed)
|
||||||
|
self.assertEqual(
|
||||||
|
fixed["additional_special_tokens"], ["<|im_start|>", "<|im_end|>"]
|
||||||
|
)
|
||||||
|
self.assertEqual(fixed["chat_template"], QWEN_CHAT_TEMPLATE)
|
||||||
|
self.assertEqual((view / "model.safetensors").read_bytes(), b"sample-weights")
|
||||||
|
self.assertTrue((view / ".modelhub-compat-report.json").is_file())
|
||||||
|
|
||||||
|
def test_no_change_does_not_create_view(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source = self._model(
|
||||||
|
root,
|
||||||
|
{"eos_token": "<|im_end|>", "chat_template": "custom template"},
|
||||||
|
)
|
||||||
|
self.assertIsNone(prepare_model(source, work_root=root))
|
||||||
|
self.assertEqual([p.name for p in root.iterdir()], ["model"])
|
||||||
|
|
||||||
|
def test_known_tokenizer_class_is_replaced_only_with_tokenizer_json(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source = self._model(
|
||||||
|
root,
|
||||||
|
{"tokenizer_class": "TokenizersBackend", "chat_template": "custom"},
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(CompatibilityError, "tokenizer.json"):
|
||||||
|
prepare_model(source, work_root=root)
|
||||||
|
(source / "tokenizer.json").write_text("{}", encoding="utf-8")
|
||||||
|
view = Path(prepare_model(source, work_root=root))
|
||||||
|
fixed = json.loads((view / "tokenizer_config.json").read_text("utf-8"))
|
||||||
|
self.assertEqual(fixed["tokenizer_class"], "PreTrainedTokenizerFast")
|
||||||
|
|
||||||
|
def test_unknown_model_family_does_not_get_qwen_template(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
source = self._model(root, {"eos_token": "<|im_end|>"})
|
||||||
|
(source / "config.json").write_text(
|
||||||
|
json.dumps({"model_type": "llama"}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
self.assertIsNone(prepare_model(source, work_root=root))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
25
vllm_wrapper.sh
Normal file
25
vllm_wrapper.sh
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
vllm_real="$(dirname "$(readlink -f "$0")")/vllm_real"
|
||||||
|
|
||||||
|
if [[ "${1:-}" != "serve" || -z "${2:-}" ]]; then
|
||||||
|
exec "$vllm_real" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
model_dir="$2"
|
||||||
|
shift 2
|
||||||
|
prepared="$(python3 /opt/modelhub/engine_compat.py "$model_dir")"
|
||||||
|
if [[ -z "$prepared" ]]; then
|
||||||
|
exec "$vllm_real" serve "$model_dir" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
args=("$@")
|
||||||
|
for ((i=0; i<${#args[@]}; i++)); do
|
||||||
|
if [[ "${args[i]}" == "--tokenizer" && $((i+1)) -lt ${#args[@]} && "${args[i+1]}" == "$model_dir" ]]; then
|
||||||
|
args[i+1]="$prepared"
|
||||||
|
elif [[ "${args[i]}" == "--tokenizer=$model_dir" ]]; then
|
||||||
|
args[i]="--tokenizer=$prepared"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exec "$vllm_real" serve "$prepared" "${args[@]}"
|
||||||
Reference in New Issue
Block a user