Text Generation
Transformers
Safetensors
English
Chinese
qwen3
from-scratch
pretraining
bilingual
piece-tokenizer
text-generation-inference
Instructions to use Ismantic/Summer-0.5B-S0 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Ismantic/Summer-0.5B-S0 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Ismantic/Summer-0.5B-S0")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("Ismantic/Summer-0.5B-S0") model = AutoModelForCausalLM.from_pretrained("Ismantic/Summer-0.5B-S0", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Ismantic/Summer-0.5B-S0 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Ismantic/Summer-0.5B-S0" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Ismantic/Summer-0.5B-S0", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/Ismantic/Summer-0.5B-S0
- SGLang
How to use Ismantic/Summer-0.5B-S0 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "Ismantic/Summer-0.5B-S0" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Ismantic/Summer-0.5B-S0", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "Ismantic/Summer-0.5B-S0" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Ismantic/Summer-0.5B-S0", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use Ismantic/Summer-0.5B-S0 with Docker Model Runner:
docker model run hf.co/Ismantic/Summer-0.5B-S0
Upload Summer-0.5B-S0
Browse files- .gitattributes +2 -33
- README.md +75 -0
- Summer-Tokenizer.dict.txt +0 -0
- Summer-Tokenizer.pt +3 -0
- checkpoint.py +123 -0
- config.json +19 -0
- example_load.py +34 -0
- example_vllm.py +46 -0
- generation_config.json +6 -0
- model.py +372 -0
- model.safetensors +3 -0
- requirements.txt +3 -0
- special_tokens_map.json +6 -0
- token_mapping.json +17 -0
- tokenizer.py +230 -0
- tokenizer_config.json +9 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,4 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
|
| 27 |
-
*.
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
| 1 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: transformers
|
| 4 |
+
pipeline_tag: text-generation
|
| 5 |
+
tags:
|
| 6 |
+
- qwen3
|
| 7 |
+
- from-scratch
|
| 8 |
+
- pretraining
|
| 9 |
+
- bilingual
|
| 10 |
+
- piece-tokenizer
|
| 11 |
+
language:
|
| 12 |
+
- en
|
| 13 |
+
- zh
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Summer-0.5B-S0
|
| 17 |
+
|
| 18 |
+
Summer-0.5B-S0 is a **from-scratch** bilingual (Chinese/English) base model:
|
| 19 |
+
524,336,128 parameters, trained from random initialization on
|
| 20 |
+
**12B tokens** with a self-trained 81,903-piece tokenizer.
|
| 21 |
+
|
| 22 |
+
It is *not* a fine-tune or a tokenizer-swap of any existing model. The
|
| 23 |
+
architecture follows `Qwen/Qwen3-0.6B-Base` (28 layers / hidden 1024 /
|
| 24 |
+
GQA 16:8 / head_dim 128 / tied embeddings / RoPE theta 1e6), but every weight
|
| 25 |
+
starts from `N(0, 0.02)`.
|
| 26 |
+
|
| 27 |
+
## Stage
|
| 28 |
+
|
| 29 |
+
**S0** — monolingual only. 12B tokens, Chinese/English 50:50, 45,149 steps.
|
| 30 |
+
No parallel or instruction data at any point.
|
| 31 |
+
|
| 32 |
+
## What to expect
|
| 33 |
+
|
| 34 |
+
**This is a 13B-token model.** For scale: `Qwen3-0.6B-Base` saw 36T tokens —
|
| 35 |
+
about 2,700x more. Treat the numbers below as what that budget buys, not as a
|
| 36 |
+
competitive result.
|
| 37 |
+
|
| 38 |
+
| WMT22 5-shot | BLEU | COMET |
|
| 39 |
+
|---|---|---|
|
| 40 |
+
| zh->en | 0.54 | 0.4638 |
|
| 41 |
+
| en->zh | 3.97 | 0.5872 |
|
| 42 |
+
|
| 43 |
+
**Few-shot translation is essentially zero, and that is the finding.** The
|
| 44 |
+
model ignores the in-context examples entirely — on zh->en it does not even
|
| 45 |
+
switch output language. Language modelling was learned; in-context learning
|
| 46 |
+
was not. 12B monolingual tokens is not enough for ICL to emerge at 0.5B.
|
| 47 |
+
|
| 48 |
+
Its value is as (a) the control for S1, and (b) a starting point for
|
| 49 |
+
mid-training / SFT.
|
| 50 |
+
|
| 51 |
+
## Tokenizer
|
| 52 |
+
|
| 53 |
+
The tokenizer is a compiled C++ extension, **not** loadable by
|
| 54 |
+
`AutoTokenizer`. The release ships `tokenizer.py` and `example_load.py`:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
pip install git+https://github.com/Ismantic/PieceTokenizer
|
| 58 |
+
python example_load.py
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
The model code (`model.py`, `checkpoint.py`) is bundled too — the package
|
| 62 |
+
depends only on `torch` plus the tokenizer extension, not on `transformers`.
|
| 63 |
+
|
| 64 |
+
## Training
|
| 65 |
+
|
| 66 |
+
Full pipeline, data mixes and every design decision (including the mistakes)
|
| 67 |
+
are documented in <https://github.com/Ismantic/Summer>. Notably
|
| 68 |
+
`docs/WHY.md` records why fp32 master weights are mandatory, why the learning
|
| 69 |
+
rate schedule is WSD rather than cosine, and what the vocabulary swap cost.
|
| 70 |
+
|
| 71 |
+
## License
|
| 72 |
+
|
| 73 |
+
Apache-2.0. Training corpora are public datasets (FineWeb-Edu, Cosmopedia,
|
| 74 |
+
CCI3-HQ, SkyPile, WMT19, OPUS-100 and others; see `data/source.py` upstream).
|
| 75 |
+
Please observe their respective licenses.
|
Summer-Tokenizer.dict.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Summer-Tokenizer.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b9b81cefcaa5d47cd3aa6e653dda0a80f90b7863b3cfff790dfc07c662dda50f
|
| 3 |
+
size 2707732
|
checkpoint.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""safetensors 读写。**只依赖 torch。**
|
| 2 |
+
|
| 3 |
+
格式很简单,自己实现比拉一个依赖划算:
|
| 4 |
+
|
| 5 |
+
[0:8] u64 小端,头部 JSON 的字节数 N
|
| 6 |
+
[8:8+N] JSON:{名字: {dtype, shape, data_offsets:[起, 止]}, "__metadata__": {...}}
|
| 7 |
+
[8+N:] 张量数据区,data_offsets 是相对这里的偏移
|
| 8 |
+
|
| 9 |
+
## 为什么不用 safetensors 库
|
| 10 |
+
|
| 11 |
+
`src/` 只依赖 torch —— 这条约束的意义是让读者能看见每一步在做什么。
|
| 12 |
+
safetensors 的格式本身只有二十来行代码,藏进依赖里反而看不见了。
|
| 13 |
+
|
| 14 |
+
## 不能改错的地方
|
| 15 |
+
|
| 16 |
+
**读进来的 key 必须与 HF 的 Qwen3ForCausalLM 完全一致。** 已发布的
|
| 17 |
+
`Ismantic/Qwen3-1.7B-Base-ReTok` 是标准 HF 命名,key 对不上不会报错 ——
|
| 18 |
+
`load_state_dict(strict=False)` 会静默跳过,模型照样随机初始化跑起来、
|
| 19 |
+
loss 照样降,只是那份权重白搭了。所以这里默认 `strict=True`。
|
| 20 |
+
|
| 21 |
+
**tie_word_embeddings 的模型没有 `lm_head.weight`。** v18 的 checkpoint 就是
|
| 22 |
+
这样:310 个张量,只有 `model.embed_tokens.weight` 和 `model.norm.weight` 加
|
| 23 |
+
28 层 ×11。加载时要把 lm_head 绑回 embed,不能指望文件里有。
|
| 24 |
+
"""
|
| 25 |
+
import json
|
| 26 |
+
import struct
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
|
| 31 |
+
# safetensors 的 dtype 名 → torch dtype
|
| 32 |
+
_DTYPES = {
|
| 33 |
+
"BF16": torch.bfloat16, "F16": torch.float16, "F32": torch.float32,
|
| 34 |
+
"F64": torch.float64, "I64": torch.int64, "I32": torch.int32,
|
| 35 |
+
"I16": torch.int16, "I8": torch.int8, "U8": torch.uint8,
|
| 36 |
+
"BOOL": torch.bool,
|
| 37 |
+
}
|
| 38 |
+
_NAMES = {v: k for k, v in _DTYPES.items()}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def read_header(path) -> tuple[dict, int]:
|
| 42 |
+
"""返回 (头部 dict, 数据区起始偏移)。不读张量,几微秒。"""
|
| 43 |
+
with open(path, "rb") as f:
|
| 44 |
+
(n,) = struct.unpack("<Q", f.read(8))
|
| 45 |
+
hdr = json.loads(f.read(n))
|
| 46 |
+
return hdr, 8 + n
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def load_safetensors(path, device="cpu") -> dict[str, torch.Tensor]:
|
| 50 |
+
"""读成 {名字: 张量}。
|
| 51 |
+
|
| 52 |
+
按 data_offsets 顺序 seek+read,每个张量一份可写内存。
|
| 53 |
+
不用 mmap:`torch.frombuffer` 不接受只读缓冲区,套一层 `bytearray` 反正
|
| 54 |
+
要拷一份,mmap 的零拷贝就没了 —— 还得处理 memoryview 挂着导出引用、
|
| 55 |
+
`mm.close()` 抛 BufferError 的麻烦。
|
| 56 |
+
"""
|
| 57 |
+
path = Path(path)
|
| 58 |
+
hdr, base = read_header(path)
|
| 59 |
+
hdr.pop("__metadata__", None)
|
| 60 |
+
|
| 61 |
+
# 按文件内偏移排序后顺序读,避免来回 seek
|
| 62 |
+
items = sorted(hdr.items(), key=lambda kv: kv[1]["data_offsets"][0])
|
| 63 |
+
out = {}
|
| 64 |
+
with open(path, "rb") as f:
|
| 65 |
+
for name, spec in items:
|
| 66 |
+
lo, hi = spec["data_offsets"]
|
| 67 |
+
f.seek(base + lo)
|
| 68 |
+
buf = bytearray(hi - lo)
|
| 69 |
+
if f.readinto(buf) != hi - lo:
|
| 70 |
+
raise EOFError(f"{path}: 读 {name} 时文件提前结束")
|
| 71 |
+
t = torch.frombuffer(buf, dtype=_DTYPES[spec["dtype"]])
|
| 72 |
+
out[name] = t.reshape(spec["shape"]).to(device)
|
| 73 |
+
return out
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def save_safetensors(tensors: dict[str, torch.Tensor], path, metadata=None) -> None:
|
| 77 |
+
"""写出。张量按名字排序,同样的输入产生同样的字节。
|
| 78 |
+
|
| 79 |
+
共享存储的张量(比如 tie 的 embed / lm_head)要**调用方自己去重** ——
|
| 80 |
+
这里不猜。写两份不会报错,只是文件大一倍、加载时可能对不上 tie 的预期。
|
| 81 |
+
"""
|
| 82 |
+
hdr, blobs, off = {}, [], 0
|
| 83 |
+
for name in sorted(tensors):
|
| 84 |
+
t = tensors[name].detach().cpu().contiguous()
|
| 85 |
+
if t.dtype not in _NAMES:
|
| 86 |
+
raise TypeError(f"{name}: 不支持的 dtype {t.dtype}")
|
| 87 |
+
b = t.view(torch.uint8).reshape(-1).numpy().tobytes()
|
| 88 |
+
hdr[name] = {"dtype": _NAMES[t.dtype], "shape": list(t.shape),
|
| 89 |
+
"data_offsets": [off, off + len(b)]}
|
| 90 |
+
blobs.append(b)
|
| 91 |
+
off += len(b)
|
| 92 |
+
if metadata:
|
| 93 |
+
hdr["__metadata__"] = {str(k): str(v) for k, v in metadata.items()}
|
| 94 |
+
|
| 95 |
+
raw = json.dumps(hdr, separators=(",", ":")).encode("utf-8")
|
| 96 |
+
# 头部按 8 字节对齐,张量数据区才能对齐
|
| 97 |
+
pad = (-len(raw)) % 8
|
| 98 |
+
raw += b" " * pad
|
| 99 |
+
|
| 100 |
+
path = Path(path)
|
| 101 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
with open(path, "wb") as f:
|
| 103 |
+
f.write(struct.pack("<Q", len(raw)))
|
| 104 |
+
f.write(raw)
|
| 105 |
+
for b in blobs:
|
| 106 |
+
f.write(b)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def load_sharded(model_dir, device="cpu") -> dict[str, torch.Tensor]:
|
| 110 |
+
"""加载一个模型目录:单文件 `model.safetensors`,或分片 + index.json。"""
|
| 111 |
+
model_dir = Path(model_dir)
|
| 112 |
+
single = model_dir / "model.safetensors"
|
| 113 |
+
if single.exists():
|
| 114 |
+
return load_safetensors(single, device)
|
| 115 |
+
|
| 116 |
+
index = model_dir / "model.safetensors.index.json"
|
| 117 |
+
if index.exists():
|
| 118 |
+
shards = sorted(set(json.loads(index.read_text())["weight_map"].values()))
|
| 119 |
+
out = {}
|
| 120 |
+
for s in shards:
|
| 121 |
+
out.update(load_safetensors(model_dir / s, device))
|
| 122 |
+
return out
|
| 123 |
+
raise FileNotFoundError(f"{model_dir} 下既没有 model.safetensors 也没有 index.json")
|
config.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"vocab_size": 81903,
|
| 3 |
+
"hidden_size": 1024,
|
| 4 |
+
"intermediate_size": 3072,
|
| 5 |
+
"num_hidden_layers": 28,
|
| 6 |
+
"num_attention_heads": 16,
|
| 7 |
+
"num_key_value_heads": 8,
|
| 8 |
+
"head_dim": 128,
|
| 9 |
+
"rms_norm_eps": 1e-06,
|
| 10 |
+
"rope_theta": 1000000,
|
| 11 |
+
"max_position_embeddings": 32768,
|
| 12 |
+
"tie_word_embeddings": true,
|
| 13 |
+
"torch_dtype": "bfloat16",
|
| 14 |
+
"initializer_range": 0.02,
|
| 15 |
+
"model_type": "qwen3",
|
| 16 |
+
"architectures": [
|
| 17 |
+
"Qwen3ForCausalLM"
|
| 18 |
+
]
|
| 19 |
+
}
|
example_load.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""跑通这个发布包 —— 只需要 torch 和 piece_tokenizer。
|
| 2 |
+
|
| 3 |
+
pip install torch
|
| 4 |
+
pip install git+https://github.com/Ismantic/PieceTokenizer
|
| 5 |
+
python example_load.py
|
| 6 |
+
"""
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from model import Qwen3ForCausalLM
|
| 10 |
+
from tokenizer import PieceTokenizerWrapper
|
| 11 |
+
|
| 12 |
+
HERE = "."
|
| 13 |
+
|
| 14 |
+
tok = PieceTokenizerWrapper(HERE)
|
| 15 |
+
model = Qwen3ForCausalLM.from_pretrained(
|
| 16 |
+
HERE, device="cuda" if torch.cuda.is_available() else "cpu",
|
| 17 |
+
dtype=torch.bfloat16)
|
| 18 |
+
|
| 19 |
+
prompt = "机器翻译的基本任务是"
|
| 20 |
+
ids = tok.encode(prompt, add_special_tokens=False)
|
| 21 |
+
print(f"{prompt!r} -> {len(ids)} tokens")
|
| 22 |
+
|
| 23 |
+
# 贪心续写 40 步。没有 KV cache —— 每步重算前缀,短续写够用。
|
| 24 |
+
x = torch.tensor([ids], device=next(model.parameters()).device)
|
| 25 |
+
out = []
|
| 26 |
+
with torch.no_grad():
|
| 27 |
+
for _ in range(40):
|
| 28 |
+
nxt = int(model(x)[0, -1].argmax())
|
| 29 |
+
if nxt == tok.eos_token_id:
|
| 30 |
+
break
|
| 31 |
+
out.append(nxt)
|
| 32 |
+
x = torch.cat([x, torch.tensor([[nxt]], device=x.device)], dim=1)
|
| 33 |
+
|
| 34 |
+
print(prompt + tok.decode(out))
|
example_vllm.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""用 vLLM 跑这个模型。
|
| 2 |
+
|
| 3 |
+
pip install vllm
|
| 4 |
+
pip install git+https://github.com/Ismantic/PieceTokenizer
|
| 5 |
+
python example_vllm.py
|
| 6 |
+
|
| 7 |
+
## 为什么要 skip_tokenizer_init
|
| 8 |
+
|
| 9 |
+
vLLM 能加载**权重** —— config.json 里 architectures 是 Qwen3ForCausalLM,
|
| 10 |
+
它用自己那份 Qwen3 实现,按 state_dict 的 key 名灌进去。
|
| 11 |
+
|
| 12 |
+
但它**认不了这个词表**:81903 的 piece 词表不是 HF 标准格式,AutoTokenizer
|
| 13 |
+
走不通。所以要 skip_tokenizer_init=True,自己编码好 token id 用 TokensPrompt
|
| 14 |
+
喂进去,拿回来的 id 自己解码。
|
| 15 |
+
|
| 16 |
+
推论:`vllm serve` 那种 OpenAI 兼容服务**开箱用不了** —— 它要把文本转成
|
| 17 |
+
token,而它转不了。调用方得自己传 token id。
|
| 18 |
+
"""
|
| 19 |
+
import torch
|
| 20 |
+
from vllm import LLM, SamplingParams
|
| 21 |
+
from vllm.inputs import TokensPrompt
|
| 22 |
+
|
| 23 |
+
from tokenizer import PieceTokenizerWrapper
|
| 24 |
+
|
| 25 |
+
HERE = "."
|
| 26 |
+
|
| 27 |
+
tok = PieceTokenizerWrapper(HERE)
|
| 28 |
+
llm = LLM(model=HERE, skip_tokenizer_init=True, dtype="bfloat16",
|
| 29 |
+
gpu_memory_utilization=0.85, max_model_len=4096,
|
| 30 |
+
trust_remote_code=True)
|
| 31 |
+
|
| 32 |
+
prompts = [
|
| 33 |
+
"机器翻译的基本任务是",
|
| 34 |
+
"中文分词的目标是",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
outputs = llm.generate(
|
| 38 |
+
[TokensPrompt(prompt_token_ids=tok.encode(p, add_special_tokens=False))
|
| 39 |
+
for p in prompts],
|
| 40 |
+
SamplingParams(temperature=0.0, max_tokens=64,
|
| 41 |
+
stop_token_ids=[tok.eos_token_id]),
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
for prompt, out in zip(prompts, outputs):
|
| 45 |
+
print(f"\n{prompt}", end="")
|
| 46 |
+
print(tok.decode(list(out.outputs[0].token_ids)))
|
generation_config.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 1,
|
| 3 |
+
"eos_token_id": 2,
|
| 4 |
+
"pad_token_id": 81899,
|
| 5 |
+
"max_new_tokens": 2048
|
| 6 |
+
}
|
model.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Qwen3 稠密模型,纯 torch 实现。**只依赖 torch。**
|
| 2 |
+
|
| 3 |
+
替掉 `transformers.Qwen3ForCausalLM`。规模参考(v18 用的 1.7B):
|
| 4 |
+
|
| 5 |
+
28 层 / hidden 2048 / intermediate 6144 / 16 个 q 头 / 8 个 kv 头(GQA)
|
| 6 |
+
head_dim 128 / RMSNorm eps 1e-6 / RoPE θ=1e6 / SwiGLU / 全程无 bias
|
| 7 |
+
词表 81903 / 输入输出嵌入绑权重
|
| 8 |
+
|
| 9 |
+
## 不能改错的地方
|
| 10 |
+
|
| 11 |
+
**state_dict 的 key 必须与 HF 的 `Qwen3ForCausalLM` 完全一致。** 已发布的
|
| 12 |
+
`Ismantic/Qwen3-1.7B-Base-ReTok` 用的是标准 HF 命名;改模块名或嵌套层级会让
|
| 13 |
+
那份权重全部失配,**而模型照样能随机初始化跑起来、loss 照样降、不报错**。
|
| 14 |
+
`test/test_model_equiv.py` 是这条的防线:同权重、与 transformers 逐层对齐。
|
| 15 |
+
|
| 16 |
+
对应关系(每层 11 个张量,共 28×11+2=310):
|
| 17 |
+
|
| 18 |
+
model.embed_tokens.weight → embed_tokens
|
| 19 |
+
model.layers.{i}.input_layernorm.weight → layers[i].input_layernorm
|
| 20 |
+
model.layers.{i}.self_attn.{q,k,v,o}_proj.weight
|
| 21 |
+
model.layers.{i}.self_attn.{q,k}_norm.weight ← Qwen3 特有,逐头 RMSNorm
|
| 22 |
+
model.layers.{i}.post_attention_layernorm.weight
|
| 23 |
+
model.layers.{i}.mlp.{gate,up,down}_proj.weight
|
| 24 |
+
model.norm.weight → norm
|
| 25 |
+
|
| 26 |
+
**没有 `lm_head.weight`。** tie_word_embeddings=true,权重与 embed 共享。
|
| 27 |
+
|
| 28 |
+
**RoPE 用 half-split(NeoX)约定,不是 interleave。** 两种都"能跑",但结果
|
| 29 |
+
完全不同 —— 用错了 loss 会明显偏高而不会报错。见 `_rotate_half`。
|
| 30 |
+
|
| 31 |
+
**RMSNorm 的中间计算在 float32。** 直接用 bf16 算方差会有肉眼可见的偏差,
|
| 32 |
+
同样不报错。
|
| 33 |
+
"""
|
| 34 |
+
import json
|
| 35 |
+
import math
|
| 36 |
+
from dataclasses import dataclass
|
| 37 |
+
from pathlib import Path
|
| 38 |
+
|
| 39 |
+
import torch
|
| 40 |
+
import torch.nn as nn
|
| 41 |
+
import torch.nn.functional as F
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _checkpoint_mod():
|
| 46 |
+
"""`src.checkpoint`,或发布包里平铺的 `checkpoint`。
|
| 47 |
+
|
| 48 |
+
发布包(save/releases/<repo>/)是平铺的 —— model.py 和 checkpoint.py 并列,
|
| 49 |
+
没有包结构,相对 import 会 ImportError。所以两种都试。
|
| 50 |
+
"""
|
| 51 |
+
try:
|
| 52 |
+
from . import checkpoint as _c # 仓库内:src 是 package
|
| 53 |
+
except ImportError: # 发布包内:平铺
|
| 54 |
+
import checkpoint as _c # noqa: I001
|
| 55 |
+
return _c
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class Qwen3Config:
|
| 60 |
+
vocab_size: int = 81903
|
| 61 |
+
hidden_size: int = 2048
|
| 62 |
+
intermediate_size: int = 6144
|
| 63 |
+
num_hidden_layers: int = 28
|
| 64 |
+
num_attention_heads: int = 16
|
| 65 |
+
num_key_value_heads: int = 8
|
| 66 |
+
head_dim: int = 128
|
| 67 |
+
rms_norm_eps: float = 1e-6
|
| 68 |
+
rope_theta: float = 1_000_000.0
|
| 69 |
+
max_position_embeddings: int = 32768
|
| 70 |
+
tie_word_embeddings: bool = True
|
| 71 |
+
torch_dtype: str = "bfloat16"
|
| 72 |
+
# 随机初始化的标准差。只有从零训练(init_weights)会读它;从 checkpoint
|
| 73 |
+
# 加载时权重来自文件,这个值不参与。与 Qwen3 官方 config 的同名字段一致。
|
| 74 |
+
initializer_range: float = 0.02
|
| 75 |
+
|
| 76 |
+
@classmethod
|
| 77 |
+
def from_json(cls, path) -> "Qwen3Config":
|
| 78 |
+
raw = json.loads(Path(path).read_text())
|
| 79 |
+
if raw.get("model_type") not in (None, "qwen3"):
|
| 80 |
+
raise ValueError(f"不是 qwen3 架构:model_type={raw.get('model_type')}")
|
| 81 |
+
# head_dim 在有些 config 里缺省,按 hidden/heads 推
|
| 82 |
+
raw.setdefault("head_dim",
|
| 83 |
+
raw["hidden_size"] // raw["num_attention_heads"])
|
| 84 |
+
raw.setdefault("torch_dtype", raw.get("dtype", "bfloat16"))
|
| 85 |
+
fields = {f for f in cls.__dataclass_fields__}
|
| 86 |
+
return cls(**{k: v for k, v in raw.items() if k in fields})
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def dtype(self) -> torch.dtype:
|
| 90 |
+
return {"bfloat16": torch.bfloat16, "float16": torch.float16,
|
| 91 |
+
"float32": torch.float32}[self.torch_dtype]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class RMSNorm(nn.Module):
|
| 95 |
+
"""与 `Qwen3RMSNorm` 逐位对齐:方差在 float32 里算,再转回原 dtype 乘权重。"""
|
| 96 |
+
|
| 97 |
+
def __init__(self, dim: int, eps: float):
|
| 98 |
+
super().__init__()
|
| 99 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 100 |
+
self.eps = eps
|
| 101 |
+
|
| 102 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 103 |
+
dt = x.dtype
|
| 104 |
+
x = x.to(torch.float32)
|
| 105 |
+
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 106 |
+
return self.weight * x.to(dt)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class RotaryEmbedding(nn.Module):
|
| 110 |
+
"""RoPE。inv_freq 存成 buffer(非持久化 —— 它是算出来的,不进 state_dict)。"""
|
| 111 |
+
|
| 112 |
+
def __init__(self, head_dim: int, theta: float):
|
| 113 |
+
super().__init__()
|
| 114 |
+
inv = 1.0 / (theta ** (torch.arange(0, head_dim, 2, dtype=torch.float32)
|
| 115 |
+
/ head_dim))
|
| 116 |
+
self.register_buffer("inv_freq", inv, persistent=False)
|
| 117 |
+
|
| 118 |
+
@torch.no_grad()
|
| 119 |
+
def forward(self, x: torch.Tensor, position_ids: torch.Tensor):
|
| 120 |
+
# position_ids [B,T] → freqs [B,T,head_dim/2] → emb [B,T,head_dim]
|
| 121 |
+
inv = self.inv_freq[None, :, None].to(x.device).float() # [1,D/2,1]
|
| 122 |
+
pos = position_ids[:, None, :].float() # [B,1,T]
|
| 123 |
+
|
| 124 |
+
# **必须强制 float32,不能让 autocast 把这个 matmul 降到 bf16。**
|
| 125 |
+
# bf16 尾数只有 8 位,整数超过 256 就不精确:257→256、1023→1024。
|
| 126 |
+
# 位置编码一错,长序列上的注意力全歪 —— 而且不报错,短序列还测不出来。
|
| 127 |
+
# 实测代价:seq_len=1024 上 next-token loss 从 2.3331 涨到 2.7605。
|
| 128 |
+
# HF 的 Qwen3RotaryEmbedding 里也有这条防护(注释写着 Force float32)。
|
| 129 |
+
dev = x.device.type if x.device.type != "mps" else "cpu"
|
| 130 |
+
with torch.autocast(device_type=dev, enabled=False):
|
| 131 |
+
freqs = (inv.float() @ pos.float()).transpose(1, 2) # [B,T,D/2]
|
| 132 |
+
emb = torch.cat((freqs, freqs), dim=-1) # [B,T,D]
|
| 133 |
+
cos, sin = emb.cos(), emb.sin()
|
| 134 |
+
return cos.to(x.dtype), sin.to(x.dtype)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 138 |
+
"""half-split(NeoX)约定:后半取负拼到前面。
|
| 139 |
+
|
| 140 |
+
**不是 interleave(GPT-J)约定。** 两种都能跑通、都不报错,但结果不同。
|
| 141 |
+
HF 的 Qwen3 用的是这个。
|
| 142 |
+
"""
|
| 143 |
+
x1, x2 = x[..., : x.shape[-1] // 2], x[..., x.shape[-1] // 2:]
|
| 144 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _apply_rope(q, k, cos, sin):
|
| 148 |
+
cos, sin = cos.unsqueeze(1), sin.unsqueeze(1) # [B,1,T,D]
|
| 149 |
+
return (q * cos + _rotate_half(q) * sin,
|
| 150 |
+
k * cos + _rotate_half(k) * sin)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _repeat_kv(x: torch.Tensor, n: int) -> torch.Tensor:
|
| 154 |
+
"""GQA:把 kv 头复制 n 份对齐 q 头数。n=1 时直接返回。"""
|
| 155 |
+
if n == 1:
|
| 156 |
+
return x
|
| 157 |
+
b, h, t, d = x.shape
|
| 158 |
+
return x[:, :, None].expand(b, h, n, t, d).reshape(b, h * n, t, d)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class Attention(nn.Module):
|
| 162 |
+
def __init__(self, cfg: Qwen3Config):
|
| 163 |
+
super().__init__()
|
| 164 |
+
self.n_head = cfg.num_attention_heads
|
| 165 |
+
self.n_kv = cfg.num_key_value_heads
|
| 166 |
+
self.head_dim = cfg.head_dim
|
| 167 |
+
self.n_rep = self.n_head // self.n_kv
|
| 168 |
+
self.scaling = self.head_dim ** -0.5
|
| 169 |
+
|
| 170 |
+
h, d = cfg.hidden_size, cfg.head_dim
|
| 171 |
+
self.q_proj = nn.Linear(h, self.n_head * d, bias=False)
|
| 172 |
+
self.k_proj = nn.Linear(h, self.n_kv * d, bias=False)
|
| 173 |
+
self.v_proj = nn.Linear(h, self.n_kv * d, bias=False)
|
| 174 |
+
self.o_proj = nn.Linear(self.n_head * d, h, bias=False)
|
| 175 |
+
# Qwen3 特有:对每个头的 d 维做 RMSNorm,在 RoPE 之前
|
| 176 |
+
self.q_norm = RMSNorm(d, cfg.rms_norm_eps)
|
| 177 |
+
self.k_norm = RMSNorm(d, cfg.rms_norm_eps)
|
| 178 |
+
|
| 179 |
+
def forward(self, x, cos, sin):
|
| 180 |
+
b, t, _ = x.shape
|
| 181 |
+
# 顺序要紧:proj → view 成 [B,T,H,D] → 逐头 norm → transpose → RoPE
|
| 182 |
+
q = self.q_norm(self.q_proj(x).view(b, t, self.n_head, self.head_dim)).transpose(1, 2)
|
| 183 |
+
k = self.k_norm(self.k_proj(x).view(b, t, self.n_kv, self.head_dim)).transpose(1, 2)
|
| 184 |
+
v = self.v_proj(x).view(b, t, self.n_kv, self.head_dim).transpose(1, 2)
|
| 185 |
+
|
| 186 |
+
q, k = _apply_rope(q, k, cos, sin)
|
| 187 |
+
k, v = _repeat_kv(k, self.n_rep), _repeat_kv(v, self.n_rep)
|
| 188 |
+
|
| 189 |
+
o = F.scaled_dot_product_attention(q, k, v, is_causal=True,
|
| 190 |
+
scale=self.scaling)
|
| 191 |
+
return self.o_proj(o.transpose(1, 2).reshape(b, t, -1))
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
class MLP(nn.Module):
|
| 195 |
+
def __init__(self, cfg: Qwen3Config):
|
| 196 |
+
super().__init__()
|
| 197 |
+
h, i = cfg.hidden_size, cfg.intermediate_size
|
| 198 |
+
self.gate_proj = nn.Linear(h, i, bias=False)
|
| 199 |
+
self.up_proj = nn.Linear(h, i, bias=False)
|
| 200 |
+
self.down_proj = nn.Linear(i, h, bias=False)
|
| 201 |
+
|
| 202 |
+
def forward(self, x):
|
| 203 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class DecoderLayer(nn.Module):
|
| 207 |
+
def __init__(self, cfg: Qwen3Config):
|
| 208 |
+
super().__init__()
|
| 209 |
+
self.self_attn = Attention(cfg)
|
| 210 |
+
self.mlp = MLP(cfg)
|
| 211 |
+
self.input_layernorm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
|
| 212 |
+
self.post_attention_layernorm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
|
| 213 |
+
|
| 214 |
+
def forward(self, x, cos, sin):
|
| 215 |
+
x = x + self.self_attn(self.input_layernorm(x), cos, sin)
|
| 216 |
+
return x + self.mlp(self.post_attention_layernorm(x))
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class Qwen3Model(nn.Module):
|
| 220 |
+
"""对应 HF 的 `model.*` 那一层前缀。"""
|
| 221 |
+
|
| 222 |
+
def __init__(self, cfg: Qwen3Config):
|
| 223 |
+
super().__init__()
|
| 224 |
+
self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
|
| 225 |
+
self.layers = nn.ModuleList(DecoderLayer(cfg) for _ in range(cfg.num_hidden_layers))
|
| 226 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
|
| 227 |
+
self.rotary_emb = RotaryEmbedding(cfg.head_dim, cfg.rope_theta)
|
| 228 |
+
self.gradient_checkpointing = False
|
| 229 |
+
|
| 230 |
+
def forward(self, input_ids, position_ids=None):
|
| 231 |
+
x = self.embed_tokens(input_ids)
|
| 232 |
+
if position_ids is None:
|
| 233 |
+
position_ids = torch.arange(input_ids.shape[1],
|
| 234 |
+
device=input_ids.device)[None]
|
| 235 |
+
cos, sin = self.rotary_emb(x, position_ids)
|
| 236 |
+
for layer in self.layers:
|
| 237 |
+
if self.gradient_checkpointing and self.training:
|
| 238 |
+
# use_reentrant=False:新式实现,能和 requires_grad=False 的
|
| 239 |
+
# 冻结参数共存(Phase 1 冻结 transformer 时正是这个场景)
|
| 240 |
+
x = torch.utils.checkpoint.checkpoint(
|
| 241 |
+
layer, x, cos, sin, use_reentrant=False)
|
| 242 |
+
else:
|
| 243 |
+
x = layer(x, cos, sin)
|
| 244 |
+
return self.norm(x)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
class Qwen3ForCausalLM(nn.Module):
|
| 248 |
+
def __init__(self, cfg: Qwen3Config):
|
| 249 |
+
super().__init__()
|
| 250 |
+
self.config = cfg
|
| 251 |
+
self.model = Qwen3Model(cfg)
|
| 252 |
+
self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
|
| 253 |
+
if cfg.tie_word_embeddings:
|
| 254 |
+
self.lm_head.weight = self.model.embed_tokens.weight
|
| 255 |
+
|
| 256 |
+
def forward(self, input_ids, position_ids=None):
|
| 257 |
+
return self.lm_head(self.model(input_ids, position_ids))
|
| 258 |
+
|
| 259 |
+
def gradient_checkpointing_enable(self, enable: bool = True) -> None:
|
| 260 |
+
self.model.gradient_checkpointing = enable
|
| 261 |
+
|
| 262 |
+
@torch.no_grad()
|
| 263 |
+
def init_weights(self, seed: int | None = None) -> "Qwen3ForCausalLM":
|
| 264 |
+
"""从零训练用的随机初始化。**没有这一步就只有 torch 的默认值。**
|
| 265 |
+
|
| 266 |
+
torch 的默认值对 transformer 是错的,而且错得不报错:
|
| 267 |
+
|
| 268 |
+
nn.Embedding → N(0, 1) std 比该有的 0.02 大 **50 倍**
|
| 269 |
+
nn.Linear → U(±1/√fan_in) 均匀分布,且 fan_in 一变标准差就变
|
| 270 |
+
|
| 271 |
+
嵌入 std=1 意味着第一层的 RMSNorm 之前激活就是 O(1)*50,而 tie 之后
|
| 272 |
+
lm_head 也是同一份权重 —— 初始 logits 的量级直接决定第一步的 loss 和
|
| 273 |
+
梯度。这类错**照样能训**,只是慢、抖、有时候崩,回头查不出原因。
|
| 274 |
+
|
| 275 |
+
用的是 Llama / GPT-NeoX 那套:
|
| 276 |
+
|
| 277 |
+
所有权重 N(0, initializer_range)
|
| 278 |
+
残差出口 N(0, initializer_range / √(2L)) ← o_proj 和 down_proj
|
| 279 |
+
RMSNorm 全 1
|
| 280 |
+
|
| 281 |
+
残差出口缩放是为了让 28 层累加之后的方差不随深度线性增长。少了它
|
| 282 |
+
深层的激活会越叠越大,表现为训练早期的 loss spike。
|
| 283 |
+
|
| 284 |
+
tie 的模型里 `lm_head.weight` 就是 `embed_tokens.weight` 同一个张量,
|
| 285 |
+
`named_parameters()` 默认去重,所以只会被初始化一次 —— 不要改成
|
| 286 |
+
`remove_duplicate=False`,那会按两套 std 初始化同一块内存。
|
| 287 |
+
"""
|
| 288 |
+
if seed is not None:
|
| 289 |
+
torch.manual_seed(seed)
|
| 290 |
+
std = self.config.initializer_range
|
| 291 |
+
resid_std = std / math.sqrt(2 * self.config.num_hidden_layers)
|
| 292 |
+
for name, p in self.named_parameters():
|
| 293 |
+
if name.endswith("o_proj.weight") or name.endswith("down_proj.weight"):
|
| 294 |
+
nn.init.normal_(p, mean=0.0, std=resid_std)
|
| 295 |
+
elif "norm" in name or "layernorm" in name:
|
| 296 |
+
nn.init.ones_(p)
|
| 297 |
+
else:
|
| 298 |
+
nn.init.normal_(p, mean=0.0, std=std)
|
| 299 |
+
return self
|
| 300 |
+
|
| 301 |
+
@classmethod
|
| 302 |
+
def from_config(cls, config_path, device="cpu", dtype=None,
|
| 303 |
+
seed: int | None = None) -> "Qwen3ForCausalLM":
|
| 304 |
+
"""只读 config.json,随机初始化权重 —— 从零预训练的起点。
|
| 305 |
+
|
| 306 |
+
与 `from_pretrained` 的区别就是不加载 checkpoint。刻意做成两个入口:
|
| 307 |
+
「加载失败于是静默随机初始化」是这个项目最怕的一类错。
|
| 308 |
+
"""
|
| 309 |
+
cfg = Qwen3Config.from_json(Path(config_path) / "config.json")
|
| 310 |
+
model = cls(cfg).to(dtype or torch.float32)
|
| 311 |
+
model.init_weights(seed=seed)
|
| 312 |
+
if cfg.tie_word_embeddings:
|
| 313 |
+
model.lm_head.weight = model.model.embed_tokens.weight
|
| 314 |
+
return model.to(device)
|
| 315 |
+
|
| 316 |
+
def save_pretrained(self, out_dir) -> None:
|
| 317 |
+
"""存成 HF 布局:`config.json` + `model.safetensors`。
|
| 318 |
+
|
| 319 |
+
tie 的模型不写 `lm_head.weight` —— 与上游 checkpoint 一致(310 个张量)。
|
| 320 |
+
分词器产物(piece.model / dict.txt / token_mapping.json)由调用方负责
|
| 321 |
+
拷过来,这里不碰文本相关的东西。
|
| 322 |
+
"""
|
| 323 |
+
import dataclasses
|
| 324 |
+
|
| 325 |
+
save_safetensors = _checkpoint_mod().save_safetensors
|
| 326 |
+
|
| 327 |
+
out_dir = Path(out_dir)
|
| 328 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 329 |
+
sd = self.state_dict_for_save()
|
| 330 |
+
cfg = dataclasses.asdict(self.config)
|
| 331 |
+
cfg["model_type"] = "qwen3"
|
| 332 |
+
cfg["architectures"] = ["Qwen3ForCausalLM"]
|
| 333 |
+
# torch_dtype 写实际存下来的精度,不写 config 里那个。
|
| 334 |
+
# 从零预训练的参数是 float32,而 config 从模板继承来的可能还是
|
| 335 |
+
# bfloat16 —— 对不上时 vLLM 按 config 说的加载,权重被悄悄转一次,
|
| 336 |
+
# 不报错。以文件里的张量为准才是唯一不会骗人的口径。
|
| 337 |
+
cfg["torch_dtype"] = str(next(iter(sd.values())).dtype).removeprefix("torch.")
|
| 338 |
+
(out_dir / "config.json").write_text(json.dumps(cfg, indent=2))
|
| 339 |
+
save_safetensors(sd, out_dir / "model.safetensors", metadata={"format": "pt"})
|
| 340 |
+
|
| 341 |
+
@classmethod
|
| 342 |
+
def from_pretrained(cls, model_dir, device="cpu", dtype=None):
|
| 343 |
+
"""从 HF 格式的目录加载。key 对不上就报错,不静默跳过。"""
|
| 344 |
+
load_sharded = _checkpoint_mod().load_sharded
|
| 345 |
+
|
| 346 |
+
model_dir = Path(model_dir)
|
| 347 |
+
cfg = Qwen3Config.from_json(model_dir / "config.json")
|
| 348 |
+
model = cls(cfg).to(dtype or cfg.dtype)
|
| 349 |
+
|
| 350 |
+
sd = load_sharded(model_dir, device="cpu")
|
| 351 |
+
# tie 的 checkpoint 里没有 lm_head.weight —— 补上指向 embed 的引用,
|
| 352 |
+
# 这样 strict=True 才不会误报缺失
|
| 353 |
+
if cfg.tie_word_embeddings and "lm_head.weight" not in sd:
|
| 354 |
+
sd["lm_head.weight"] = sd["model.embed_tokens.weight"]
|
| 355 |
+
missing, unexpected = model.load_state_dict(sd, strict=False)
|
| 356 |
+
# 手动检查而不是 strict=True:好给出能看懂的报错
|
| 357 |
+
if missing or unexpected:
|
| 358 |
+
raise RuntimeError(
|
| 359 |
+
f"state_dict 对不上 {model_dir}:\n"
|
| 360 |
+
f" 缺少 {len(missing)} 个:{sorted(missing)[:6]}\n"
|
| 361 |
+
f" 多余 {len(unexpected)} 个:{sorted(unexpected)[:6]}\n"
|
| 362 |
+
f" 这类错**不会自己暴露** —— 权重没加载上模型照样跑。")
|
| 363 |
+
if cfg.tie_word_embeddings:
|
| 364 |
+
model.lm_head.weight = model.model.embed_tokens.weight
|
| 365 |
+
return model.to(device).eval()
|
| 366 |
+
|
| 367 |
+
def state_dict_for_save(self) -> dict[str, torch.Tensor]:
|
| 368 |
+
"""导出成 HF 格式:tie 的话去掉 `lm_head.weight`,与上游 checkpoint 一致。"""
|
| 369 |
+
sd = self.state_dict()
|
| 370 |
+
if self.config.tie_word_embeddings:
|
| 371 |
+
sd.pop("lm_head.weight", None)
|
| 372 |
+
return sd
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d7c081f09d27487588dd88c11e5ef8734a11ca02ae9d7427fbee36b5c3d95a63
|
| 3 |
+
size 1048707384
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.6
|
| 2 |
+
# 分词器(C++,同时提供 81903 词表):
|
| 3 |
+
# pip install git+https://github.com/Ismantic/PieceTokenizer
|
special_tokens_map.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token": "<s>",
|
| 3 |
+
"eos_token": "</s>",
|
| 4 |
+
"unk_token": "<unk>",
|
| 5 |
+
"pad_token": "<pad>"
|
| 6 |
+
}
|
token_mapping.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_vocab_size": 81903,
|
| 3 |
+
"total_vocab_size": 81903,
|
| 4 |
+
"special_tokens": {
|
| 5 |
+
"<pad>": 81899,
|
| 6 |
+
"<user>": 81900,
|
| 7 |
+
"<assistant>": 81901,
|
| 8 |
+
"<system>": 81902
|
| 9 |
+
},
|
| 10 |
+
"bos_id": 1,
|
| 11 |
+
"eos_id": 2,
|
| 12 |
+
"unk_id": 0,
|
| 13 |
+
"pad_id": 81899,
|
| 14 |
+
"user_id": 81900,
|
| 15 |
+
"assistant_id": 81901,
|
| 16 |
+
"system_id": 81902
|
| 17 |
+
}
|
tokenizer.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PieceTokenizer 的 HuggingFace 风格外壳。
|
| 2 |
+
|
| 3 |
+
## dict.txt 是必需的,不是可选的
|
| 4 |
+
|
| 5 |
+
没有中文分词词典时,**中文的 token id 会变** —— 不只是慢。2026-07-27 实测
|
| 6 |
+
(同一 piece.model,带/不带 dict):
|
| 7 |
+
|
| 8 |
+
中文 22 字 0.169ms → 1.060ms 6.3× id 序列不同(长度都是 22)
|
| 9 |
+
中文 ×20 3.289ms → 21.125ms 6.4× id 序列不同(长度都是 440)
|
| 10 |
+
英文 0.110ms → 0.110ms 1.0× 完全相同(dict 只管中文切分)
|
| 11 |
+
|
| 12 |
+
**round-trip 正确会掩盖这个问题**:两种模式 decode 都能还原原文,看起来没事,
|
| 13 |
+
但喂给模型的 id 已经不是训练时那套了。所以这里缺 dict 直接报错,不静默降级
|
| 14 |
+
(旧版本是 `else: load(model_file)`,不报错 —— 那是个真隐患)。
|
| 15 |
+
|
| 16 |
+
确实不需要 dict 的场合(比如只查 vocab_size)传 `require_dict=False`。
|
| 17 |
+
|
| 18 |
+
## 词表从哪来
|
| 19 |
+
|
| 20 |
+
本仓库**不存词表副本**。规范位置是 clone 的 PieceTokenizer:
|
| 21 |
+
|
| 22 |
+
deps/PieceTokenizer/save/Summer-Tokenizer.pt
|
| 23 |
+
deps/PieceTokenizer/save/Summer-Tokenizer.dict.txt
|
| 24 |
+
|
| 25 |
+
用 `resolve_assets()` 反查(经 `piece_tokenizer.__file__`)。checkpoint 目录里
|
| 26 |
+
另有一份 piece.model + dict.txt,与上面逐字节相同(sha256 已核),训练/评测时
|
| 27 |
+
直接从 checkpoint 目录加载即可。
|
| 28 |
+
"""
|
| 29 |
+
import os
|
| 30 |
+
import json
|
| 31 |
+
import piece_tokenizer as pt
|
| 32 |
+
|
| 33 |
+
PIECE_MODEL_NAME = "Summer-Tokenizer.pt"
|
| 34 |
+
PIECE_DICT_NAME = "Summer-Tokenizer.dict.txt"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def resolve_assets():
|
| 38 |
+
"""从已安装的 piece_tokenizer 反查词表文件,返回 (piece_model, cn_dict)。
|
| 39 |
+
|
| 40 |
+
本仓库不留副本,所以要经 `piece_tokenizer.__file__` 找到 clone 的仓库根,
|
| 41 |
+
再取 `save/` 下那两个文件。找不到就报错 —— 报错好过静默用错词表。
|
| 42 |
+
|
| 43 |
+
`SUMMER_PIECE_MODEL` / `SUMMER_PIECE_DICT` 可以覆盖(与 BERTc 的
|
| 44 |
+
`BERTC_PIECE_MODEL` 同一套做法)。
|
| 45 |
+
"""
|
| 46 |
+
env_m = os.environ.get("SUMMER_PIECE_MODEL")
|
| 47 |
+
env_d = os.environ.get("SUMMER_PIECE_DICT")
|
| 48 |
+
if env_m and env_d:
|
| 49 |
+
return env_m, env_d
|
| 50 |
+
|
| 51 |
+
root = os.path.dirname(os.path.abspath(pt.__file__))
|
| 52 |
+
tried = []
|
| 53 |
+
for cand in (root, os.path.dirname(root)):
|
| 54 |
+
save = os.path.join(cand, "save")
|
| 55 |
+
model, cn_dict = (os.path.join(save, PIECE_MODEL_NAME),
|
| 56 |
+
os.path.join(save, PIECE_DICT_NAME))
|
| 57 |
+
tried.append(save)
|
| 58 |
+
if os.path.exists(model) and os.path.exists(cn_dict):
|
| 59 |
+
return model, cn_dict
|
| 60 |
+
raise FileNotFoundError(
|
| 61 |
+
f"找不到 {PIECE_MODEL_NAME} / {PIECE_DICT_NAME}。找过:{tried}\n"
|
| 62 |
+
f" 先跑 `bash prepare/install_deps.sh` clone 并安装 PieceTokenizer。")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# checkpoint 目录里词表文件的名字。**上游名优先。**
|
| 66 |
+
#
|
| 67 |
+
# `Summer-Tokenizer.pt` / `.dict.txt` 与 PieceTokenizer 仓库 `save/` 下的文件
|
| 68 |
+
# 同名 —— 这样从任何一个 checkpoint 都能一眼看出词表出自哪里,不用靠 sha256
|
| 69 |
+
# 去反推(BERTc 一直是这么做的,它的发布包里就叫 `BERTc-Tokenizer.pt`)。
|
| 70 |
+
#
|
| 71 |
+
# `piece.model` / `dict.txt` 是改造之前的名字。已发布的
|
| 72 |
+
# `Ismantic/Qwen3-1.7B-Base-ReTok` 和 v18 的 checkpoint 用的都是旧名,所以
|
| 73 |
+
# **保留为回退**,不然那些目录一个都加载不了。新产出统一用上游名。
|
| 74 |
+
_MODEL_NAMES = (PIECE_MODEL_NAME, "piece.model", "piece_mt.model")
|
| 75 |
+
_DICT_NAMES = (PIECE_DICT_NAME, "dict.txt")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _first_in(model_dir, names):
|
| 79 |
+
for n in names:
|
| 80 |
+
p = os.path.join(model_dir, n)
|
| 81 |
+
if os.path.exists(p):
|
| 82 |
+
return p
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def has_piece_vocab(model_dir) -> bool:
|
| 87 |
+
"""这个目录是 piece 词表的模型吗?
|
| 88 |
+
|
| 89 |
+
评测入口靠它决定用 PieceTokenizerWrapper 还是 AutoTokenizer。**不要各自
|
| 90 |
+
硬编码文件名** —— 新产出用上游名 `Summer-Tokenizer.pt`,旧的用
|
| 91 |
+
`piece.model`,漏一个就会静默退回 AutoTokenizer,而 AutoTokenizer 对这个
|
| 92 |
+
词表走不通,结果是错的。
|
| 93 |
+
"""
|
| 94 |
+
return _first_in(model_dir, _MODEL_NAMES) is not None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class PieceTokenizerWrapper:
|
| 98 |
+
def __init__(self, model_dir, require_dict=True):
|
| 99 |
+
"""从模型目录加载。
|
| 100 |
+
|
| 101 |
+
词表文件按 `_MODEL_NAMES` / `_DICT_NAMES` 的顺序找 —— 上游名优先,
|
| 102 |
+
旧名回退。
|
| 103 |
+
"""
|
| 104 |
+
self._tok = pt.Tokenizer()
|
| 105 |
+
|
| 106 |
+
model_file = _first_in(model_dir, _MODEL_NAMES)
|
| 107 |
+
if model_file is None:
|
| 108 |
+
raise FileNotFoundError(
|
| 109 |
+
f"{model_dir} 里找不到词表。试过:{list(_MODEL_NAMES)}")
|
| 110 |
+
|
| 111 |
+
cn_dict = _first_in(model_dir, _DICT_NAMES)
|
| 112 |
+
if cn_dict is not None:
|
| 113 |
+
self._tok.load(model_file, cn_dict)
|
| 114 |
+
elif require_dict:
|
| 115 |
+
raise FileNotFoundError(
|
| 116 |
+
f"{model_dir} 里没有中文分词词典(试过 {list(_DICT_NAMES)})。"
|
| 117 |
+
f"缺了它中文的 token id 会变(不只是慢),而且 decode 照样能"
|
| 118 |
+
f"还原原文、不会报错。\n"
|
| 119 |
+
f" 从 checkpoint 或 PieceTokenizer 的 save/{PIECE_DICT_NAME} "
|
| 120 |
+
f"拷���份过来;确实不需要就传 require_dict=False。")
|
| 121 |
+
else:
|
| 122 |
+
self._tok.load(model_file)
|
| 123 |
+
self.piece_model_path = model_file
|
| 124 |
+
self.cn_dict_path = cn_dict
|
| 125 |
+
|
| 126 |
+
# Load token mapping
|
| 127 |
+
mapping_file = os.path.join(model_dir, "token_mapping.json")
|
| 128 |
+
if os.path.exists(mapping_file):
|
| 129 |
+
with open(mapping_file) as f:
|
| 130 |
+
mapping = json.load(f)
|
| 131 |
+
self.pad_token_id = mapping["pad_id"]
|
| 132 |
+
self.bos_token_id = mapping["bos_id"]
|
| 133 |
+
self.eos_token_id = mapping["eos_id"]
|
| 134 |
+
self.user_token_id = mapping.get("user_id")
|
| 135 |
+
self.assistant_token_id = mapping.get("assistant_id")
|
| 136 |
+
self.system_token_id = mapping.get("system_id")
|
| 137 |
+
else:
|
| 138 |
+
# Fallback to piece_to_id lookups
|
| 139 |
+
self.bos_token_id = self._tok.piece_to_id("<s>")
|
| 140 |
+
self.eos_token_id = self._tok.piece_to_id("</s>")
|
| 141 |
+
self.pad_token_id = self._tok.piece_to_id("<pad>")
|
| 142 |
+
self.user_token_id = self._tok.piece_to_id("<user>")
|
| 143 |
+
self.assistant_token_id = self._tok.piece_to_id("<assistant>")
|
| 144 |
+
self.system_token_id = self._tok.piece_to_id("<system>")
|
| 145 |
+
if self.pad_token_id < 0:
|
| 146 |
+
self.pad_token_id = 0
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def vocab_size(self):
|
| 150 |
+
return self._tok.vocab_size()
|
| 151 |
+
|
| 152 |
+
def encode(self, text, add_special_tokens=False):
|
| 153 |
+
ids = self._tok.encode_as_ids(text)
|
| 154 |
+
if add_special_tokens:
|
| 155 |
+
ids = [self.bos_token_id] + ids + [self.eos_token_id]
|
| 156 |
+
return ids
|
| 157 |
+
|
| 158 |
+
def decode(self, ids, skip_special_tokens=True):
|
| 159 |
+
if skip_special_tokens:
|
| 160 |
+
special = {self.bos_token_id, self.eos_token_id, self.pad_token_id,
|
| 161 |
+
self.user_token_id, self.assistant_token_id, self.system_token_id}
|
| 162 |
+
ids = [i for i in ids if i not in special]
|
| 163 |
+
try:
|
| 164 |
+
return self._tok.decode(ids)
|
| 165 |
+
except UnicodeDecodeError:
|
| 166 |
+
# Model emitted byte-fallback piece(s) that don't form valid UTF-8.
|
| 167 |
+
# Per-piece fallback: keep ids that decode cleanly, drop the rest.
|
| 168 |
+
parts = []
|
| 169 |
+
for i in ids:
|
| 170 |
+
try:
|
| 171 |
+
parts.append(self._tok.id_to_piece(i))
|
| 172 |
+
except UnicodeDecodeError:
|
| 173 |
+
continue
|
| 174 |
+
return "".join(parts).replace("▁", " ")
|
| 175 |
+
|
| 176 |
+
def apply_chat_template(self, messages, tokenize=True, add_generation_prompt=False, **kwargs):
|
| 177 |
+
"""Build chat-formatted token sequence from messages."""
|
| 178 |
+
ids = []
|
| 179 |
+
|
| 180 |
+
# Check for system message
|
| 181 |
+
start = 0
|
| 182 |
+
if messages and messages[0]["role"] == "system":
|
| 183 |
+
ids.append(self.bos_token_id)
|
| 184 |
+
ids.extend(self._tok.encode_as_ids(messages[0]["content"]))
|
| 185 |
+
ids.append(self.system_token_id)
|
| 186 |
+
start = 1
|
| 187 |
+
else:
|
| 188 |
+
ids.append(self.bos_token_id)
|
| 189 |
+
|
| 190 |
+
for msg in messages[start:]:
|
| 191 |
+
if msg["role"] == "user":
|
| 192 |
+
ids.append(self.user_token_id)
|
| 193 |
+
ids.extend(self._tok.encode_as_ids(msg["content"]))
|
| 194 |
+
elif msg["role"] == "assistant":
|
| 195 |
+
ids.append(self.assistant_token_id)
|
| 196 |
+
ids.extend(self._tok.encode_as_ids(msg["content"]))
|
| 197 |
+
ids.append(self.eos_token_id)
|
| 198 |
+
|
| 199 |
+
if add_generation_prompt:
|
| 200 |
+
ids.append(self.assistant_token_id)
|
| 201 |
+
|
| 202 |
+
if tokenize:
|
| 203 |
+
return ids
|
| 204 |
+
else:
|
| 205 |
+
# Return as string (rarely needed)
|
| 206 |
+
return self._tok.decode(ids)
|
| 207 |
+
|
| 208 |
+
def save_pretrained(self, output_dir):
|
| 209 |
+
"""Save tokenizer files to directory (for checkpoint saving)."""
|
| 210 |
+
import shutil
|
| 211 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 212 |
+
# Copy piece.model
|
| 213 |
+
# 从自己加载时的实际路径拷,不猜文件名
|
| 214 |
+
if self.piece_model_path:
|
| 215 |
+
shutil.copy2(self.piece_model_path,
|
| 216 |
+
os.path.join(output_dir, PIECE_MODEL_NAME))
|
| 217 |
+
if self.cn_dict_path:
|
| 218 |
+
shutil.copy2(self.cn_dict_path,
|
| 219 |
+
os.path.join(output_dir, PIECE_DICT_NAME))
|
| 220 |
+
# Save mapping
|
| 221 |
+
mapping = {
|
| 222 |
+
"bos_id": self.bos_token_id,
|
| 223 |
+
"eos_id": self.eos_token_id,
|
| 224 |
+
"pad_id": self.pad_token_id,
|
| 225 |
+
"user_id": self.user_token_id,
|
| 226 |
+
"assistant_id": self.assistant_token_id,
|
| 227 |
+
"system_id": self.system_token_id,
|
| 228 |
+
}
|
| 229 |
+
with open(os.path.join(output_dir, "token_mapping.json"), "w") as f:
|
| 230 |
+
json.dump(mapping, f, indent=2)
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "qwen3",
|
| 3 |
+
"bos_token": "<s>",
|
| 4 |
+
"eos_token": "</s>",
|
| 5 |
+
"unk_token": "<unk>",
|
| 6 |
+
"pad_token": "<pad>",
|
| 7 |
+
"clean_up_tokenization_spaces": false,
|
| 8 |
+
"tokenizer_class": "PreTrainedTokenizerFast"
|
| 9 |
+
}
|