This commit is contained in:
187
scripts/apply_patches.py
Executable file
187
scripts/apply_patches.py
Executable file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把 R2 chat_template 与 R4 architecture 白名单应用到模型目录。
|
||||
|
||||
不访问平台,不启动 vLLM。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
FORBIDDEN_FRAMEWORK = "vllm_fix_tokenizer"
|
||||
TOKENIZER_PATCH_KEY = "modelhub_tokenizer_patch"
|
||||
|
||||
|
||||
class PatchError(Exception):
|
||||
"""模型目录不满足本引擎的修补前提。"""
|
||||
|
||||
|
||||
def engine_root() -> Path:
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def assert_framework(framework: str) -> None:
|
||||
if not framework or framework == FORBIDDEN_FRAMEWORK:
|
||||
raise PatchError(
|
||||
"FRAMEWORK 必须是 vllm_0_17_0_corex_4_4_0,"
|
||||
"不能使用 vllm_fix_tokenizer"
|
||||
)
|
||||
|
||||
|
||||
def load_whitelist(path: Path) -> set[str]:
|
||||
names: set[str] = set()
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
item = line.split("#", 1)[0].strip()
|
||||
if item:
|
||||
names.add(item)
|
||||
if not names:
|
||||
raise PatchError(f"架构白名单为空: {path}")
|
||||
return names
|
||||
|
||||
|
||||
def load_aliases(path: Path) -> dict[str, str]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise PatchError(f"别名文件必须是对象: {path}")
|
||||
return {str(key): str(value) for key, value in data.items()}
|
||||
|
||||
|
||||
def preview_default_template(messages: list[dict[str, str]], add_generation_prompt: bool = True) -> str:
|
||||
"""与 default_chat_template.jinja 同一套角色标记,供无 Jinja 依赖的本地预览。"""
|
||||
parts: list[str] = []
|
||||
for message in messages:
|
||||
role = message["role"]
|
||||
content = message["content"]
|
||||
parts.append(f"<|{role}|>\n{content}<|end|>\n")
|
||||
if add_generation_prompt:
|
||||
parts.append("<|assistant|>\n")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def apply_chat_template(model_dir: Path, template_text: str, force: bool = False) -> dict:
|
||||
path = model_dir / "tokenizer_config.json"
|
||||
if not path.is_file():
|
||||
raise PatchError(f"缺少 tokenizer_config.json: {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise PatchError("tokenizer_config.json 必须是对象")
|
||||
current = data.get("chat_template")
|
||||
if isinstance(current, str) and current.strip() and not force:
|
||||
return {"injected": False, "reason": "已有 chat_template"}
|
||||
data["chat_template"] = template_text
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return {"injected": True, "reason": "已写入 chat_template"}
|
||||
|
||||
|
||||
def apply_architectures(
|
||||
model_dir: Path,
|
||||
whitelist: set[str],
|
||||
aliases: dict[str, str],
|
||||
unknown_policy: str = "fail",
|
||||
) -> dict:
|
||||
if unknown_policy not in {"fail", "skip"}:
|
||||
raise PatchError("ARCH_UNKNOWN_POLICY 只能是 fail 或 skip")
|
||||
path = model_dir / "config.json"
|
||||
if not path.is_file():
|
||||
raise PatchError(f"缺少 config.json: {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise PatchError("config.json 必须是对象")
|
||||
architectures = data.get("architectures")
|
||||
if not isinstance(architectures, list) or not architectures:
|
||||
raise PatchError("config.json 缺少 architectures")
|
||||
|
||||
rewritten: list[str] = []
|
||||
skipped: list[str] = []
|
||||
updated: list[str] = []
|
||||
for name in architectures:
|
||||
if not isinstance(name, str) or not name:
|
||||
raise PatchError("architectures 含有空名称")
|
||||
if name in whitelist:
|
||||
updated.append(name)
|
||||
continue
|
||||
target = aliases.get(name)
|
||||
if target in whitelist:
|
||||
updated.append(target)
|
||||
rewritten.append(f"{name}->{target}")
|
||||
continue
|
||||
if unknown_policy == "skip":
|
||||
skipped.append(name)
|
||||
updated.append(name)
|
||||
continue
|
||||
raise PatchError(f"未知架构: {name}")
|
||||
|
||||
changed = updated != architectures
|
||||
if changed:
|
||||
data["architectures"] = updated
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return {
|
||||
"architectures": updated,
|
||||
"rewritten": rewritten,
|
||||
"skipped": skipped,
|
||||
"changed": changed,
|
||||
}
|
||||
|
||||
|
||||
def apply_tokenizer_patch(model_dir: Path, enabled: bool) -> dict:
|
||||
if not enabled:
|
||||
return {"applied": False}
|
||||
path = model_dir / "tokenizer_config.json"
|
||||
if not path.is_file():
|
||||
raise PatchError(f"缺少 tokenizer_config.json: {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
data[TOKENIZER_PATCH_KEY] = "optional-v1"
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return {"applied": True}
|
||||
|
||||
|
||||
def apply_all(
|
||||
model_dir: Path,
|
||||
template_text: str,
|
||||
whitelist: set[str],
|
||||
aliases: dict[str, str],
|
||||
unknown_policy: str = "fail",
|
||||
force_chat_template: bool = False,
|
||||
tokenizer_patch: bool = False,
|
||||
) -> dict:
|
||||
chat = apply_chat_template(model_dir, template_text, force=force_chat_template)
|
||||
architectures = apply_architectures(model_dir, whitelist, aliases, unknown_policy)
|
||||
tokenizer = apply_tokenizer_patch(model_dir, tokenizer_patch)
|
||||
return {"chat_template": chat, "architectures": architectures, "tokenizer": tokenizer}
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
root = engine_root()
|
||||
parser = argparse.ArgumentParser(description="应用 R2/R4 模型目录修补")
|
||||
parser.add_argument("--model-dir", required=True, type=Path)
|
||||
parser.add_argument("--framework", default="vllm_0_17_0_corex_4_4_0")
|
||||
parser.add_argument("--unknown-policy", default="fail", choices=("fail", "skip"))
|
||||
parser.add_argument("--force-chat-template", action="store_true")
|
||||
parser.add_argument("--tokenizer-patch", action="store_true")
|
||||
parser.add_argument("--template", type=Path, default=root / "patches/chat_template/default_chat_template.jinja")
|
||||
parser.add_argument("--whitelist", type=Path, default=root / "patches/architectures/whitelist.txt")
|
||||
parser.add_argument("--aliases", type=Path, default=root / "patches/architectures/aliases.json")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
assert_framework(args.framework)
|
||||
result = apply_all(
|
||||
args.model_dir,
|
||||
args.template.read_text(encoding="utf-8"),
|
||||
load_whitelist(args.whitelist),
|
||||
load_aliases(args.aliases),
|
||||
unknown_policy=args.unknown_policy,
|
||||
force_chat_template=args.force_chat_template,
|
||||
tokenizer_patch=args.tokenizer_patch,
|
||||
)
|
||||
except (PatchError, OSError, json.JSONDecodeError) as exc:
|
||||
print(f"修补失败: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
125
scripts/entrypoint.sh
Executable file
125
scripts/entrypoint.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# 读取 task_info.env,修补模型目录,并准备 vLLM 启动命令。
|
||||
# --dry-run 只打印计划,不启动进程,不访问平台。
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
usage() {
|
||||
echo "用法: entrypoint.sh [--dry-run]" >&2
|
||||
}
|
||||
|
||||
DRY_RUN=0
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
DRY_RUN=1
|
||||
elif [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
elif [[ -n "${1:-}" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info_file="${TASK_INFO_FILE:-}"
|
||||
if [[ -z "$info_file" ]]; then
|
||||
if [[ -f "${ROOT}/task_info.env" ]]; then
|
||||
info_file="${ROOT}/task_info.env"
|
||||
else
|
||||
info_file="${ROOT}/task_info.env.example"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$info_file" ]]; then
|
||||
echo "找不到任务信息文件: ${info_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 已在环境中的变量优先于文件,方便本地覆盖。
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
case "$line" in
|
||||
""|\#*) continue ;;
|
||||
esac
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
if [[ -z "$key" || "$key" == "$line" ]]; then
|
||||
echo "任务信息行无法解析: ${line}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${!key+x}" ]]; then
|
||||
export "${key}=${value}"
|
||||
fi
|
||||
done <"$info_file"
|
||||
|
||||
FRAMEWORK="${FRAMEWORK:-}"
|
||||
GPU_TYPE="${GPU_TYPE:-}"
|
||||
TASK_TYPE="${TASK_TYPE:-text-generation}"
|
||||
MODEL_DIR="${MODEL_DIR:-/model}"
|
||||
SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-llm}"
|
||||
PORT="${PORT:-8000}"
|
||||
ARCH_UNKNOWN_POLICY="${ARCH_UNKNOWN_POLICY:-fail}"
|
||||
APPLY_TOKENIZER_PATCH="${APPLY_TOKENIZER_PATCH:-0}"
|
||||
FORCE_CHAT_TEMPLATE="${FORCE_CHAT_TEMPLATE:-0}"
|
||||
|
||||
if [[ -z "$FRAMEWORK" || "$FRAMEWORK" == "vllm_fix_tokenizer" ]]; then
|
||||
echo "拒绝 FRAMEWORK=${FRAMEWORK:-空}。本仓使用 vllm_0_17_0_corex_4_4_0。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$GPU_TYPE" ]]; then
|
||||
echo "GPU_TYPE 为空" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
plan=(
|
||||
python3 -m vllm.entrypoints.openai.api_server
|
||||
--model "$MODEL_DIR"
|
||||
--port "$PORT"
|
||||
--served-model-name "$SERVED_MODEL_NAME"
|
||||
--trust-remote-code
|
||||
)
|
||||
|
||||
echo "FRAMEWORK=${FRAMEWORK}"
|
||||
echo "GPU_TYPE=${GPU_TYPE}"
|
||||
echo "TASK_TYPE=${TASK_TYPE}"
|
||||
echo "MODEL_DIR=${MODEL_DIR}"
|
||||
echo "ARCH_UNKNOWN_POLICY=${ARCH_UNKNOWN_POLICY}"
|
||||
echo "plan: ${plan[*]}"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
preview="$(
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
root = Path(sys.argv[1])
|
||||
sys.path.insert(0, str(root / "scripts"))
|
||||
import apply_patches
|
||||
messages = [
|
||||
{"role": "system", "content": "你是助手"},
|
||||
{"role": "user", "content": "你好"},
|
||||
]
|
||||
print(apply_patches.preview_default_template(messages).replace("\n", "\\n"))
|
||||
PY
|
||||
)"
|
||||
echo "template_preview: ${preview}"
|
||||
echo "dry-run: 不启动进程,不访问平台"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "$MODEL_DIR" ]]; then
|
||||
echo "模型目录不存在: ${MODEL_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
patch_args=(
|
||||
--model-dir "$MODEL_DIR"
|
||||
--framework "$FRAMEWORK"
|
||||
--unknown-policy "$ARCH_UNKNOWN_POLICY"
|
||||
)
|
||||
if [[ "$FORCE_CHAT_TEMPLATE" == "1" ]]; then
|
||||
patch_args+=(--force-chat-template)
|
||||
fi
|
||||
if [[ "$APPLY_TOKENIZER_PATCH" == "1" ]]; then
|
||||
patch_args+=(--tokenizer-patch)
|
||||
fi
|
||||
|
||||
python3 "${ROOT}/scripts/apply_patches.py" "${patch_args[@]}"
|
||||
exec "${plan[@]}"
|
||||
Reference in New Issue
Block a user