按成本而非频次挑选宏 token

受 Dharma-AI 的 VocabX 文章启发,在模拟的作文评分请求上比较两种新增宏 token 的挑法:按省下的 token 数(频次)挑,还是按省下的钱(输出 token 单价为输入的 7.4 倍)挑,观察在不同预算下的 token 减少、加权成本减少和回复长度。

已验证 Python 3.12.15 耗时 10.3s 运行于 2026-10-22 22:09 UTC
结论

宏 token 预算为 4/8/16 时,按成本挑选比按频次多省 24.4/6.1/1.8 个百分点的加权成本

检查 7/7 通过
  • K=4:按频次挑选省下的 token 总数不少于按成本挑选
  • K=4:按成本挑选省下的加权成本更多
  • K=8:按频次挑选省下的 token 总数不少于按成本挑选
  • K=8:按成本挑选省下的加权成本更多
  • K=16:按频次挑选省下的 token 总数不少于按成本挑选
  • K=16:按成本挑选省下的加权成本更多
  • 预算越紧,两种挑法的成本差距越大
实验指标(%)
K=4 按频次 加权成本减少7.5%
K=4 按成本 加权成本减少31.9%
K=8 按频次 加权成本减少35.1%
K=8 按成本 加权成本减少41.2%
K=16 按频次 加权成本减少53.5%
K=16 按成本 加权成本减少55.3%

数值取自 GitHub Actions 实际运行输出中的 METRIC 行

实验指标(token)
K=4 按频次 平均回复长度25.5token
K=4 按成本 平均回复长度14.0token
K=8 按频次 平均回复长度16.5token
K=8 按成本 平均回复长度10.7token
K=16 按频次 平均回复长度10.0token
K=16 按成本 平均回复长度9.0token

数值取自 GitHub Actions 实际运行输出中的 METRIC 行

以上内容从实际运行输出中提取,完整输出见下方。

python main.py
语料:300 条请求,原始 31045 个 token,加权成本 80005,平均回复 25.5 token
K= 4 按频次:token 减少 19.3%,加权成本减少 7.5%,平均回复 25.5 token;前 2 个宏 token ['you are a strict essay grader', '<M0> . read the essay and']
K= 4 按成本:token 减少 11.1%,加权成本减少 31.9%,平均回复 14.0 token;前 2 个宏 token [', " feedback " : "', '{ " grade " :']
K= 8 按频次:token 减少 34.8%,加权成本减少 35.1%,平均回复 16.5 token;前 2 个宏 token ['you are a strict essay grader', '<M0> . read the essay and']
K= 8 按成本:token 减少 14.3%,加权成本减少 41.2%,平均回复 10.7 token;前 2 个宏 token [', " feedback " : "', '{ " grade " :']
K=16 按频次:token 减少 42.0%,加权成本减少 53.5%,平均回复 10.0 token;前 2 个宏 token ['you are a strict essay grader', '<M0> . read the essay and']
K=16 按成本:token 减少 40.1%,加权成本减少 55.3%,平均回复 9.0 token;前 2 个宏 token [', " feedback " : "', '{ " grade " :']
METRIC: K=4 按频次 加权成本减少 = 7.5 %
METRIC: K=4 按成本 加权成本减少 = 31.9 %
METRIC: K=8 按频次 加权成本减少 = 35.1 %
METRIC: K=8 按成本 加权成本减少 = 41.2 %
METRIC: K=16 按频次 加权成本减少 = 53.5 %
METRIC: K=16 按成本 加权成本减少 = 55.3 %
METRIC: K=4 按频次 平均回复长度 = 25.5 token
METRIC: K=4 按成本 平均回复长度 = 14.0 token
METRIC: K=8 按频次 平均回复长度 = 16.5 token
METRIC: K=8 按成本 平均回复长度 = 10.7 token
METRIC: K=16 按频次 平均回复长度 = 10.0 token
METRIC: K=16 按成本 平均回复长度 = 9.0 token
[PASS] K=4:按频次挑选省下的 token 总数不少于按成本挑选
[PASS] K=4:按成本挑选省下的加权成本更多
[PASS] K=8:按频次挑选省下的 token 总数不少于按成本挑选
[PASS] K=8:按成本挑选省下的加权成本更多
[PASS] K=16:按频次挑选省下的 token 总数不少于按成本挑选
[PASS] K=16:按成本挑选省下的加权成本更多
[PASS] 预算越紧,两种挑法的成本差距越大
RESULT: 宏 token 预算为 4/8/16 时,按成本挑选比按频次多省 24.4/6.1/1.8 个百分点的加权成本

目的

Dharma-AI 在 Hugging Face 博客 How Specializing the Vocabulary of SLMs Can Cut Inference Cost by 77% 中介绍了 VocabX:为领域小模型重建词表时,按「省下的钱」而不是「出现频次」挑选新 token,并把重复的多词片段合并成一个「宏 token」。在他们的场景里,省下一个回复(输出)token 的价值是省下一个输入 token 的 7.4 倍。

本实验用一个极简的贪心算法演示:在同样的宏 token 预算下,两种挑法各自压缩了什么、省了多少。

注意:语料是脚本按固定随机种子生成的模拟「作文评分」请求,分词用空格切词近似,不涉及真实模型、真实分词器或 VocabX 的实现;7.4 倍的价格比照搬自原文的案例,只作设定。

步骤

  1. 生成 300 条请求,每条包含固定的系统提示、随机的作文正文(输入),以及 JSON 格式的评分回复(输出)
  2. 在每个字段内统计 2–6 个词的 n-gram,贪心地每次挑一个最优片段合并为宏 token,再重新统计: - 按频次:分数 =(片段长度 − 1)× 出现次数 - 按成本:分数 =(片段长度 − 1)× Σ 各字段出现次数 × 字段单价(输入 1,输出 7.4)
  3. 预算分别取 4、8、16 个宏 token,比较 token 减少、加权成本减少和平均回复长度

运行:

python main.py

程序打印 METRIC: 指标、[PASS]/[FAIL] 检查和最后一行 RESULT:,任一检查失败时以非零状态退出。

预期结果

  • 按频次挑选会先压缩每条请求都重复的系统提示,省下的 token 总数更多
  • 按成本挑选会先压缩回复里的 JSON 结构,加权成本降得更多、回复更短
  • 预算越紧,两种挑法的成本差距越大

局限与可以继续尝试

  • 真实部署中系统提示往往能被提示词缓存覆盖,输入侧的实际价格会更低,这会进一步拉大两种挑法的差距
  • 原文提醒:把承载决策的内容(如分数)和结构一起合并成一个 token,会损害质量;本实验只算成本,不评估质量
  • 可以把 make_corpus 换成你自己的请求日志(脱敏后),把单价换成实际的输入 / 输出价格

内网复现

  • 无需任何第三方依赖,只用 Python 3.8+ 标准库,无需联网,可直接在内网运行
  • 把 main.py 拷贝到内网机器执行 python main.py 即可
  • 若要在真实分词器上尝试类似思路,需要通过内网 PyPI 镜像安装 tokenizers 等库,并在内网模型仓库中准备基础模型的分词器文件

代码

main.py146 行
"""按「成本」而不是「频次」挑选宏 token 的最小演示。

受 Dharma-AI 的 VocabX 文章启发:同样新增 K 个宏 token(把一段重复的词序列合并成一个 token),
如果按「省下的 token 数」挑,会优先压缩最常出现的输入片段;
如果按「省下的钱」挑(输出 token 单价是输入的 7.4 倍),会优先压缩模型输出里的重复结构。

语料是本脚本用固定随机种子生成的模拟「作文评分」请求,不是真实数据;
分词用空格切词近似,不涉及真实模型或分词器。
"""
from __future__ import annotations

import random
import sys
from collections import Counter

OUTPUT_PRICE = 7.4  # 一个输出 token 的价格(以输入 token 为 1)
INPUT_PRICE = 1.0
BUDGETS = (4, 8, 16)  # 允许新增的宏 token 个数
MAX_N = 6

SYSTEM = (
    "you are a strict essay grader . read the essay and return a json object "
    "with the grade and short feedback . grade from 0 to 10 ."
)
TOPICS = ["school", "family", "travel", "science", "music", "sport", "city", "nature"]
ADJ = ["clear", "weak", "strong", "vague", "good", "poor"]
ISSUES = ["the argument is", "the structure is", "the conclusion is", "the grammar is"]


def make_corpus(n: int, seed: int = 7):
    rng = random.Random(seed)
    records = []
    for _ in range(n):
        topic = rng.choice(TOPICS)
        words = [rng.choice(TOPICS + ADJ + ["the", "a", "is", "and", "my", "i", "we"]) for _ in range(rng.randint(30, 60))]
        user = f"essay about {topic} : " + " ".join(words)
        grade = rng.randint(0, 10)
        fb = " ; ".join(f"{rng.choice(ISSUES)} {rng.choice(ADJ)}" for _ in range(rng.randint(2, 3)))
        resp = f'{{ " grade " : {grade} , " feedback " : " {fb} " }}'
        records.append({"system": SYSTEM.split(), "user": user.split(), "response": resp.split()})
    return records


WEIGHT = {"system": INPUT_PRICE, "user": INPUT_PRICE, "response": OUTPUT_PRICE}


def totals(records):
    tok = sum(len(r[f]) for r in records for f in WEIGHT)
    cost = sum(len(r[f]) * WEIGHT[f] for r in records for f in WEIGHT)
    return tok, cost


def count_ngrams(records):
    """返回 {ngram: {field: 出现次数}},只统计不跨字段的 n-gram。"""
    stats: dict[tuple, Counter] = {}
    for r in records:
        for f in WEIGHT:
            seq = r[f]
            for n in range(2, MAX_N + 1):
                for i in range(len(seq) - n + 1):
                    g = tuple(seq[i:i + n])
                    stats.setdefault(g, Counter())[f] += 1
    return stats


def replace(seq, gram, sym):
    out, i, n = [], 0, len(gram)
    while i < len(seq):
        if tuple(seq[i:i + n]) == gram:
            out.append(sym)
            i += n
        else:
            out.append(seq[i])
            i += 1
    return out


def build(records, mode: str, budget: int):
    recs = [{f: list(r[f]) for f in WEIGHT} for r in records]
    chosen = []
    for k in range(budget):
        stats = count_ngrams(recs)
        best, best_score = None, 0.0
        for g, c in stats.items():
            saved = len(g) - 1
            if mode == "frequency":
                score = saved * sum(c.values())
            else:
                score = saved * sum(cnt * WEIGHT[f] for f, cnt in c.items())
            if score > best_score:
                best, best_score = g, score
        if best is None:
            break
        sym = f"<M{k}>"
        chosen.append(" ".join(best))
        for r in recs:
            for f in WEIGHT:
                r[f] = replace(r[f], best, sym)
    return recs, chosen


def main() -> int:
    records = make_corpus(300)
    base_tok, base_cost = totals(records)
    base_resp = sum(len(r["response"]) for r in records) / len(records)
    print(f"语料:{len(records)} 条请求,原始 {base_tok} 个 token,加权成本 {base_cost:.0f},平均回复 {base_resp:.1f} token")

    names = {"frequency": "按频次", "cost": "按成本"}
    res = {}
    for budget in BUDGETS:
        for mode in ("frequency", "cost"):
            recs, chosen = build(records, mode, budget)
            tok, cost = totals(recs)
            resp = sum(len(r["response"]) for r in recs) / len(recs)
            res[(budget, mode)] = ((1 - tok / base_tok) * 100, (1 - cost / base_cost) * 100, resp)
            print(f"K={budget:>2} {names[mode]}:token 减少 {res[(budget, mode)][0]:.1f}%,"
                  f"加权成本减少 {res[(budget, mode)][1]:.1f}%,平均回复 {resp:.1f} token;前 2 个宏 token {chosen[:2]}")

    for budget in BUDGETS:
        for mode in ("frequency", "cost"):
            print(f"METRIC: K={budget} {names[mode]} 加权成本减少 = {res[(budget, mode)][1]:.1f} %")
    for budget in BUDGETS:
        for mode in ("frequency", "cost"):
            print(f"METRIC: K={budget} {names[mode]} 平均回复长度 = {res[(budget, mode)][2]:.1f} token")

    ok = True
    for budget in BUDGETS:
        f, c = res[(budget, "frequency")], res[(budget, "cost")]
        checks = [
            (f[0] >= c[0], f"K={budget}:按频次挑选省下的 token 总数不少于按成本挑选"),
            (c[1] > f[1], f"K={budget}:按成本挑选省下的加权成本更多"),
        ]
        for passed, msg in checks:
            print(f"[{'PASS' if passed else 'FAIL'}] {msg}")
            ok &= passed
    gaps = [res[(b, "cost")][1] - res[(b, "frequency")][1] for b in BUDGETS]
    shrinking = all(a > b for a, b in zip(gaps, gaps[1:]))
    print(f"[{'PASS' if shrinking else 'FAIL'}] 预算越紧,两种挑法的成本差距越大")
    ok &= shrinking
    print(f"RESULT: 宏 token 预算为 {'/'.join(map(str, BUDGETS))} 时,按成本挑选比按频次多省 "
          f"{'/'.join(f'{g:.1f}' for g in gaps)} 个百分点的加权成本")
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())
requirements.txt1 行
# 无第三方依赖,只使用 Python 标准库
Esc
↑ ↓ 选择Enter 打开

键盘快捷键

j / k
下一条 / 上一条
o
展开或收起当前条目
Enter
打开当前条目详情
c
复制当前条目链接
s
收藏 / 取消收藏当前条目
/ 或 ⌘K
搜索
1 2 3
切换密度:标题 / 摘要 / 完整
t
切换主题
?
显示本帮助