Files
nano-vllm/docs/new_model_integration_guide.md
Zijie Tian a239bfb40d 📚 docs: add new model integration guide
Summarizes lessons learned from GLM-4 integration:
- Config field mapping (multi_query_group_num, kv_channels, etc.)
- RoPE variants (interleaved vs half, partial vs full rotation)
- EOS token handling for multi-EOS models
- Weight name conversion patterns
- Verification checklist

Also updates CLAUDE.md to reflect GLM-4 support.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 13:36:24 +08:00

324 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 新模型整合指南
本文档总结了将新模型如GLM-4整合到nanovllm的经验和常见问题。
## 整合流程概览
```
1. 分析模型配置 (config.json)
2. 创建模型文件 (nanovllm/models/<model>.py)
3. 实现权重加载 (nanovllm/utils/loader.py)
4. 处理特殊组件 (RoPE, Attention, etc.)
5. 处理tokenizer差异 (EOS tokens, chat template)
6. 验证输出正确性
```
---
## 1. 配置字段映射
不同模型使用不同的配置字段名称,需要建立映射关系:
| 标准字段 | GLM-4 | Qwen | Llama | 说明 |
|----------|-------|------|-------|------|
| `num_key_value_heads` | `multi_query_group_num` | `num_key_value_heads` | `num_key_value_heads` | KV heads数量 |
| `head_dim` | `kv_channels` | 计算得出 | 计算得出 | 每个head的维度 |
| `intermediate_size` | `ffn_hidden_size` | `intermediate_size` | `intermediate_size` | FFN隐藏层大小 |
| `max_position_embeddings` | `seq_length` | `max_position_embeddings` | `max_position_embeddings` | 最大位置 |
| `rope_theta` | `10000 * rope_ratio` | `rope_theta` | `rope_theta` | RoPE基础频率 |
### 代码示例
```python
# 在模型 __init__ 中处理配置差异
num_kv_heads = getattr(config, 'num_key_value_heads',
getattr(config, 'multi_query_group_num', num_heads))
head_dim = getattr(config, 'head_dim',
getattr(config, 'kv_channels', hidden_size // num_heads))
intermediate_size = getattr(config, 'intermediate_size',
getattr(config, 'ffn_hidden_size', None))
max_position = getattr(config, 'max_position_embeddings',
getattr(config, 'seq_length', 4096))
```
---
## 2. RoPE实现差异
RoPE是模型整合中**最容易出错**的部分。不同模型可能使用不同的RoPE变体
### 2.1 旋转方式
| 类型 | 描述 | 使用模型 |
|------|------|----------|
| **Half rotation** | 前半和后半分别旋转 `[x0,x1,...] → [x0*cos-x_{d/2}*sin, ...]` | Llama, Qwen |
| **Interleaved rotation** | 相邻元素配对旋转 `[x0,x1,...] → [x0*cos-x1*sin, x1*cos+x0*sin, ...]` | GLM-4 |
### 2.2 旋转维度
| 类型 | 描述 | 使用模型 |
|------|------|----------|
| **Full rotation** | 旋转整个head_dim | Llama, Qwen |
| **Partial rotation** | 只旋转head_dim的一部分其余pass-through | GLM-4 (rotary_dim = head_dim // 2) |
### 2.3 GLM-4 RoPE实现
```python
class GLM4RotaryEmbedding(nn.Module):
def __init__(self, head_dim, rotary_dim, ...):
# GLM-4只旋转一半维度
self.rotary_dim = rotary_dim # = head_dim // 2
def forward(self, positions, query, key):
# 分离旋转部分和pass-through部分
q_rot = query[..., :self.rotary_dim]
q_pass = query[..., self.rotary_dim:]
# 只对旋转部分应用interleaved RoPE
q_rot = apply_rotary_emb_interleaved(q_rot, cos, sin)
# 拼接回去
return torch.cat([q_rot, q_pass], dim=-1), ...
```
### 2.4 调试RoPE问题
**症状**:模型输出乱码或重复无意义的内容(如 "The. The. The..."
**调试方法**
```python
# 对比HuggingFace参考实现的输出
hf_q, hf_k = hf_model.apply_rotary_pos_emb(query, key, cos, sin)
my_q, my_k = my_rotary_emb(positions, query, key)
print(f"Q max diff: {(hf_q - my_q).abs().max()}") # 应该 < 1e-5
print(f"K max diff: {(hf_k - my_k).abs().max()}") # 应该 < 1e-5
```
---
## 3. 权重名称映射
不同模型的权重命名规范不同:
### 3.1 常见映射
| 组件 | Llama/Qwen | GLM-4 |
|------|------------|-------|
| Attention QKV | `q_proj`, `k_proj`, `v_proj` | `query_key_value` (合并) |
| Attention Output | `o_proj` | `dense` |
| MLP Gate | `gate_proj` | `dense_h_to_4h` (部分) |
| MLP Up | `up_proj` | `dense_h_to_4h` (部分) |
| MLP Down | `down_proj` | `dense_4h_to_h` |
| LayerNorm | `input_layernorm` | `input_layernorm` |
| Post-Attention LN | `post_attention_layernorm` | `post_attention_layernorm` |
### 3.2 实现权重转换
```python
def convert_glm4_weights(name, param):
"""将GLM-4权重名称转换为nanovllm格式"""
# 处理合并的QKV权重
if "query_key_value" in name:
# 拆分为q, k, v
q, k, v = param.split([q_size, kv_size, kv_size], dim=0)
return {"q_proj": q, "k_proj": k, "v_proj": v}
# 处理合并的gate+up权重
if "dense_h_to_4h" in name:
gate, up = param.chunk(2, dim=0)
return {"gate_proj": gate, "up_proj": up}
return {name: param}
```
---
## 4. EOS Token处理
### 4.1 问题
某些模型使用**多个EOS tokens**
| 模型 | EOS Token(s) | 说明 |
|------|--------------|------|
| Llama | `128001` | 单一EOS |
| Qwen | `151643` | 单一EOS |
| GLM-4 | `[151329, 151336, 151338]` | 多个endoftext, user, observation |
**问题**`tokenizer.eos_token_id` 只返回第一个导致模型不会在其他EOS token处停止。
### 4.2 解决方案
```python
# config.py - 支持多个EOS
eos: int | list[int] = -1
# llm_engine.py - 从hf_config读取完整EOS列表
eos_from_config = getattr(config.hf_config, 'eos_token_id', None)
if eos_from_config is not None:
config.eos = eos_from_config
else:
config.eos = self.tokenizer.eos_token_id
# scheduler.py - 使用set进行高效查找
self.eos_set = set(eos) if isinstance(eos, list) else {eos}
# 检查时使用 in 而不是 ==
if token_id in self.eos_set:
# 停止生成
```
### 4.3 调试EOS问题
**症状**模型总是生成到max_tokens才停止
**调试方法**
```python
# 检查EOS配置
print(f"tokenizer.eos_token_id: {tokenizer.eos_token_id}")
print(f"hf_config.eos_token_id: {config.hf_config.eos_token_id}")
# 检查输出中的EOS tokens
output = llm.generate([prompt], params)
for eos_id in [151329, 151336, 151338]:
if eos_id in output[0]['token_ids']:
print(f"Found EOS {eos_id} at position {output[0]['token_ids'].index(eos_id)}")
```
---
## 5. Chat Template
不同模型使用不同的对话模板:
| 模型 | 模板格式 |
|------|----------|
| Llama-3 | `<\|begin_of_text\|><\|start_header_id\|>user<\|end_header_id\|>\n{content}<\|eot_id\|><\|start_header_id\|>assistant<\|end_header_id\|>\n` |
| Qwen | `<\|im_start\|>user\n{content}<\|im_end\|>\n<\|im_start\|>assistant\n` |
| GLM-4 | `[gMASK]<sop><\|user\|>\n{content}<\|assistant\|>\n` |
### 实现模板转换
```python
def convert_to_model_prompt(prompt: str, model_type: str) -> str:
"""将标准prompt转换为模型特定格式"""
if model_type == "glm4":
return f"[gMASK]<sop><|user|>\n{prompt}<|assistant|>\n"
elif model_type == "llama3":
return f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n"
# ...
```
---
## 6. 验证清单
整合新模型后,按以下顺序验证:
### 6.1 权重加载验证
```python
# 检查所有权重是否正确加载
for name, param in model.named_parameters():
if param.abs().sum() == 0:
print(f"WARNING: {name} is all zeros!")
```
### 6.2 单层输出验证
```python
# 对比embedding层输出
my_emb = my_model.embed_tokens(input_ids)
hf_emb = hf_model.model.embed_tokens(input_ids)
print(f"Embedding diff: {(my_emb - hf_emb).abs().max()}") # < 1e-5
# 对比第一层输出
my_out = my_model.layers[0](my_emb, ...)
hf_out = hf_model.model.layers[0](hf_emb, ...)
print(f"Layer 0 diff: {(my_out - hf_out).abs().max()}") # < 1e-4
```
### 6.3 生成质量验证
```python
# 简单问答测试
prompt = "Hello, how are you?"
output = llm.generate([prompt], SamplingParams(max_tokens=50))
print(output[0]['text']) # 应该是连贯的回答
# 检查是否正确停止
print(f"Generated {len(output[0]['token_ids'])} tokens (max=50)")
```
### 6.4 RULER基准测试
```bash
# 运行1个sample快速验证
python tests/test_ruler.py --model <path> --num-samples 1
# 验证通过后运行完整测试
python tests/test_ruler.py --model <path> --num-samples 100
```
---
## 7. 常见问题速查
| 症状 | 可能原因 | 解决方案 |
|------|----------|----------|
| 输出乱码/重复 | RoPE实现错误 | 检查旋转方式(interleaved vs half)和旋转维度(full vs partial) |
| 数值爆炸(NaN/Inf) | 权重加载错误或dtype不匹配 | 检查权重映射确保dtype一致 |
| 不停止生成 | EOS token处理错误 | 从hf_config读取完整EOS列表 |
| 输出质量差 | LayerNorm或bias缺失 | 检查add_qkv_bias等配置 |
| 位置编码错误 | max_position_embeddings读取错误 | 检查配置字段名称(seq_length等) |
---
## 8. 文件结构
新模型整合需要修改/创建的文件:
```
nanovllm/
├── models/
│ └── <model>.py # 新建:模型定义
├── layers/
│ └── rotary_embedding.py # 修改如需特殊RoPE
├── utils/
│ └── loader.py # 修改:权重加载
├── config.py # 可能修改:新配置字段
└── engine/
├── llm_engine.py # 可能修改EOS处理
└── scheduler.py # 可能修改EOS检查
tests/
└── test_ruler.py # 修改chat template
```
---
## 附录GLM-4整合案例
### 遇到的问题及解决
1. **配置字段差异** → 添加getattr fallback链
2. **Interleaved RoPE** → 实现`apply_rotary_emb_interleaved`
3. **Partial rotation (head_dim//2)** → 实现`GLM4RotaryEmbedding`
4. **多EOS tokens** → 修改config/llm_engine/scheduler支持list
5. **合并的QKV权重** → 在loader中拆分
### 关键代码位置
- RoPE实现: `nanovllm/layers/rotary_embedding.py:GLM4RotaryEmbedding`
- 模型定义: `nanovllm/models/glm4.py`
- 权重加载: `nanovllm/utils/loader.py:load_glm4_weights`
- EOS处理: `nanovllm/engine/scheduler.py:eos_set`