概述
现在大模型厂家很多,每家的API价格都是以tokens作为计费单位。第一次见时,不免产生疑问:tokens究竟是啥?为什么国产化之后都没有翻译成中文,看着很怪。本文将围绕token,对输入token进行统计分析,并对本地部署的DeepSeek大模型token的输出速度进行相关测试。
1. token的定义
有人早就提出了“在中文 NLP 等论文中,应该如何翻译 token 这个词?”[1]这个问题,实际上,并不是国内厂商偷懒懒得翻译,实在是没有标准翻译方法。因为在“互联网”应用中,往往把“token”直译作令牌
,因为其往往用于身份验证代表某个用户、设备或会话的授权信息。
在NLP领域,再对token进行直译,似乎与其实际语义存在较大偏差。token实际是语言模型处理文本的基础单元,比较相近的语义是词元、词符等。但标准不明,莫衷一是,索性直接不翻译。
对于token的划分,实际上也没有统一的做法,不同的语言模型可能会使用不同的 Token 化方法[2] :
基于字符的 Token:将文本分割成单个字符,例如 “Hello” 被分割成 [“H”, “e”, “l”, “l”, “o”]。
基于单词的 Token:将文本分割成单词,例如 “Hello, world” 被分割成 [“Hello”, “world”]。
基于子词的 Token:将单词分割成更小的子词单元,例如 “unhappy” 被分割成 [“un”, “happy”]。这种方法在处理词缀和词根时非常有效。
混合 Token:一些模型会结合多种 Token 化方法,以更好地处理语言的复杂性。
2. token离线计算
DeepSeek API 文档[3] 提供了一个token计算方式,用的是LlamaTokenizerFast
的分词器,共包含三个文件:
- deepseek_tokenizer.py:分词器使用示例
- tokenizer_config.json:分词器配置信息
- tokenizer.json:包含词汇表和分词规则
我稍微修改了一下 deepseek_tokenizer.py
,使其输出更为清晰,内容如下:
import transformers
chat_tokenizer_dir = "./"
tokenizer = transformers.AutoTokenizer.from_pretrained(
chat_tokenizer_dir, trust_remote_code=True
)
text = "Hello,world!"
result = tokenizer.encode(text)
print(f"编码结果: {result}")
# 解码回文本
decoded = tokenizer.decode(result)
print(f"解码结果: {decoded}")
# 查看每个ID对应的标记
tokens = tokenizer.convert_ids_to_tokens(result)
print(f"标记列表: {tokens}")
# 详细展示每个标记及其ID
for token_id, token in zip(result, tokens):
print(f"ID: {token_id}, 标记: {token}")
输出结果如下:
编码结果: [19923, 14, 29616, 3]
解码结果: Hello,world!
标记列表: ['Hello', ',', 'world', '!']
ID: 19923, 标记: Hello
ID: 14, 标记: ,
ID: 29616, 标记: world
ID: 3, 标记: !
可以看到,它将"Hello,world!"分解成了4个tokens。
3. 本地大模型输出速率测试
如果用ollama
在本地部署大模型,可以通过eval_count
参数直接获取到具体输出的tokens数量。我写了一个测试tokens输出速率的脚本,内容如下:
import requests
import time
# ollama 的 API 地址
OLLAMA_API_URL = "http://localhost:11434/api/generate"
# 请求参数
payload = {
"model": "deepseek-r1:32b", # 替换为你的模型名称
"prompt": "目标检测的具体含义是什么?", # 替换为你的输入文本
"stream": False, # 设置为 False,一次性返回完整结果
"max_tokens": 100 # 设置生成的最大 token 数量
}
# 打印 model 和 prompt 信息
print(f"使用的模型: {payload['model']}")
print(f"输入的问题: {payload['prompt']}")
# 记录开始时间
start_time = time.time()
print(f"开始时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time))}")
# 发送请求
response = requests.post(OLLAMA_API_URL, json=payload)
# 记录结束时间
end_time = time.time()
print(f"结束时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(end_time))}")
# 解析响应
if response.status_code == 200:
result = response.json()
# print(result)
generated_text = result.get("response", "")
generated_tokens = result.get("eval_count", 0) # 获取生成的 token 数量
elapsed_time = end_time - start_time
# 计算每秒生成的 token 数量
tokens_per_second = generated_tokens / elapsed_time
print(f"模型回答: {generated_text}")
print(f"生成时间: {elapsed_time:.2f}秒")
print(f"生成 token 数量: {generated_tokens}")
print(f"每秒生成 token 数量: {tokens_per_second:.2f}")
else:
print(f"请求失败,状态码: {response.status_code}")
print(f"错误信息: {response.text}")
下面是我在四卡4090 D服务器上进行的实验效果:
模型名称 | 模型精度 | 部署方式 | 实际显存占用 | 生成tokens/秒 |
---|---|---|---|---|
deepseek-r1:32b | 4 | 单卡部署 | 20GB | 33.76 |
deepseek-r1:32b | 4 | 双卡部署 | 20GB | 32.98 |
deepseek-r1:32b | 4 | 四卡部署 | 20GB | 32.65 |
deepseek-r1:70b | 4 | 双卡部署 | 44GB | 17.68 |
deepseek-r1:70b | 4 | 四卡部署 | 44GB | 17.46 |
deepseek-r1:70b | 8 | 四卡部署 | 74GB | 11.54 |
qwen2.5:72b(非推理模型) | 4 | 四卡部署 | 49GB | 17.25 |
从实测数据中,可以得到以下三个结论:
- 多卡部署时,生成速度会受到PCIE带宽限制影响,但实际测算影响较低
- 同一参数量模型,使用更高精度推理,速度会大大降低
- 模型是否包含推理过程,对生成速率无明显影响,因为对于推理模型来说,推理过程就是输出的一部分
下面是一张主流大模型平台输出tokens速率的对比图,大部分模型的输出速率在20-40这个区间,因此,输出速率在这个范围左右,基本可以保证流畅的用户体验。
4. 节省tokens小提示
在写这篇文章时,特意看了下各大模型的API收费标准,输入tokens和输出tokens实际上是单独计费的。我的上一篇文章【大模型】如何正确评估DeepSeek-R1各版本所需推理显存?KV Cache原理和显存计算解析提到过,对于多轮对话,目前主流框架/应用都会把历史记录继续输入到大模型中。
因此,如果利用API进行部署,下一个问题和之前的问题没什么关联,可以新开一个对话,避免API用量被过度消耗。
参考
[1] 在中文 NLP 等论文中,应该如何翻译 token 这个词?:https://www.zhihu.com/question/39279003
[2] 大模型中的token:https://blog.csdn.net/xixingzhe2/article/details/145797529
[3] DeepSeek API 文档:https://api-docs.deepseek.com/zh-cn/quick_start/token_usage