推理模型从零构建 第2章:使用Qwen3生成文本

📅 2026-03-02📖 ~8 min readReasoningQwen3Text Generation
本文是 Build a Reasoning Model (From Scratch)(Sebastian Raschka 著)第2章的学习笔记。这一章是全书的起点:加载一个预训练的 Qwen3 0.6B 模型,理解 tokenizer 的 encode/decode 流程,实现最基础的逐 token 文本生成,再通过 KV Cachetorch.compile 将推理速度提升数倍。
Ch3: 评估推理模型 →

1. 全书定位与本章目标

这本书的核心主题是推理模型 (Reasoning Model) — 不是从零训练一个 LLM(那是 Raschka 上一本书 LLMs from Scratch 的内容),而是在一个已有的预训练模型之上,从零实现各种推理增强技术

Chapter 2 overview

第2章是全书基础:搭建环境、加载模型、实现文本生成 (图源: Reasoning from Scratch)

本章是 setup 章节,核心任务:

  • 加载 Qwen3 0.6B 基座模型和对应的 tokenizer
  • 理解 LLM 逐 token 自回归生成的过程
  • 编写 generate_text_basic_stream 生成函数
  • KV Cachetorch.compile 优化推理性能

为什么选 Qwen3?作者给出三个理由:(1) 截至写书时 Qwen3 是性能最强的开源模型;(2) 0.6B 参数量比 Llama 3 1B 更节省显存;(3) 同时有 base 和 reasoning 两个版本,方便做对比实验。


2. Tokenizer:文本与数字的桥梁

LLM 不认识文字,只认识数字。Tokenizer 负责将人类可读的文本转换为 token ID 序列(encode),以及将模型输出的 ID 序列还原为文本(decode)。

Tokenizer encode and decode

Tokenizer 的核心职责:encode(文本→ID)和 decode(ID→文本)(图源: Reasoning from Scratch)

书中使用作者自己用 tokenizers 库重新实现的 Qwen3Tokenizer,词汇表大小约 151,936 个 token:

Python — Tokenizer encode/decode
from pathlib import Path
from reasoning_from_scratch.qwen3 import Qwen3Tokenizer, download_qwen3_small

# 下载 tokenizer 文件
download_qwen3_small(kind="base", tokenizer_only=True, out_dir="qwen3")

# 加载 tokenizer
tokenizer_path = Path("qwen3") / "tokenizer-base.json"
tokenizer = Qwen3Tokenizer(tokenizer_file_path=tokenizer_path)

# encode: 文本 → token ID 列表
prompt = "Explain large language models."
input_token_ids = tokenizer.encode(prompt)

for i in input_token_ids:
    print(f"{i} --> {tokenizer.decode([i])}")
# 840  --> Ex
# 20772 --> plain
# 3460  -->  large
# 4128  -->  language
# 4119  -->  models
# 13    --> .

# decode: token ID 列表 → 文本(无损还原)
print(tokenizer.decode(input_token_ids))
# "Explain large language models."

ℹ️ 关键观察:注意 “Explain” 被拆成了 “Ex” + “plain” 两个子词 — 这就是 BPE (Byte Pair Encoding) 分词的特点。低频词会被拆成高频子词,确保任何输入都能被编码,不会出现 “unknown token” 问题。Qwen3 的词汇表比 GPT-2 (50,257) 大了约 3 倍,对中文和多语言的支持更好。


3. 加载预训练 Qwen3 模型

模型加载分三步:检测可用硬件 → 下载权重文件 → 实例化模型并加载权重。

3.1 设备检测

书中实现了一个 get_device 函数,按优先级检测 CUDA → MPS (Apple Silicon) → XPU (Intel) → CPU:

Python — 自动设备检测
import torch

def get_device(enable_tensor_cores=True):
    if torch.cuda.is_available():
        device = torch.device("cuda")
        # 启用 TF32 加速矩阵乘法(Ampere 及以上架构)
        if enable_tensor_cores:
            torch.backends.cuda.matmul.allow_tf32 = True
            torch.backends.cudnn.allow_tf32 = True
    elif torch.backends.mps.is_available():
        device = torch.device("mps")
    elif torch.xpu.is_available():
        device = torch.device("xpu")
    else:
        device = torch.device("cpu")
    return device

device = get_device()  # 自动选择最佳设备

3.2 模型实例化

模型权重文件约 1.5 GB。作者用纯 PyTorch 重新实现了 Qwen3 架构(详见附录 C),与官方权重完全兼容:

Python — 加载 Qwen3 0.6B
from reasoning_from_scratch.qwen3 import (
    Qwen3Model, QWEN_CONFIG_06_B, download_qwen3_small
)

# 下载预训练权重(~1.5 GB)
download_qwen3_small(kind="base", tokenizer_only=False, out_dir="qwen3")

# 实例化模型并加载权重
model_path = Path("qwen3") / "qwen3-0.6B-base.pth"
model = Qwen3Model(QWEN_CONFIG_06_B)
model.load_state_dict(torch.load(model_path))
model.to(device)
Qwen3 model architecture

Qwen3 0.6B 架构:28 层 TransformerBlock,每层包含 GQA 注意力和 SwiGLU 前馈网络 (图源: Reasoning from Scratch)

Qwen3 0.6B 的关键参数:1024 维嵌入,28 层 Transformer,16 个 query head / 8 个 KV head(GQA),词汇表 151,936。总参数量约 6 亿,bfloat16 精度下占用约 1.5 GB 显存。


4. 逐 Token 文本生成

LLM 的文本生成是一个自回归 (autoregressive) 过程:每次把当前的所有 token 喂给模型,取最后一个位置的 logits,用 argmax 选出下一个 token,拼接到输入末尾,循环往复。

Sequential text generation

自回归生成:每步产生一个新 token,然后追加到输入序列中 (图源: Reasoning from Scratch)

理解一下模型的前向传播:输入 6 个 token,模型输出 shape 为 [1, 6, 151936] — 即每个位置都产生了一个 151,936 维的 logit 向量。我们只关心最后一个位置的 logit,对其取 argmax 得到下一个 token ID。

Model output logits

取最后一个位置的 logit 向量,argmax 得到下一个 token (图源: Reasoning from Scratch)

4.1 generate_text_basic_stream

书中用 Python generator(yield)实现了一个流式生成函数,边生成边输出:

Python — 基础文本生成函数
@torch.inference_mode()
def generate_text_basic_stream(model, token_ids, max_new_tokens, eos_token_id=None):
    model.eval()

    for _ in range(max_new_tokens):
        # 前向传播,取最后一个位置的 logits
        out = model(token_ids)[:, -1]
        # 贪心解码:选概率最高的 token
        next_token = torch.argmax(out, dim=-1, keepdim=True)

        # 遇到终止符则停止
        if eos_token_id is not None and torch.all(next_token == eos_token_id):
            break

        yield next_token  # 流式输出每个 token
        # 将新 token 拼接到输入序列末尾
        token_ids = torch.cat([token_ids, next_token], dim=1)


# 使用示例
prompt = "Explain large language models in a single sentence."
input_ids = torch.tensor(tokenizer.encode(prompt), device=device).unsqueeze(0)

for token in generate_text_basic_stream(model, input_ids, max_new_tokens=100,
                                         eos_token_id=tokenizer.eos_token_id):
    print(tokenizer.decode(token.squeeze(0).tolist()), end="", flush=True)
# → Large language models are artificial intelligence systems that can
#   understand, generate, and process human language, enabling them to
#   perform a wide range of tasks, from answering questions to writing
#   articles, and even creating creative content.

⚠️ 为什么需要 eos_token_id? Base 模型在训练时,用 <|endoftext|>(ID: 151643)分隔不同文档。生成时如果不设 eos 终止条件,模型会在生成完有意义的回答后继续 “编” 下一篇文档的内容。设置 eos_token_id 让生成函数在遇到该 token 时及时停止。


5. KV Cache 加速推理

上面的基础版本有一个严重的效率问题:每生成一个新 token,都要重新计算所有之前 token 的 key 和 value。这些已有 token 的 attention 中间结果其实不会变化 — 缓存它们可以避免大量重复计算。

KV cache concept

KV Cache:缓存已计算的 key/value,每步只需计算新 token 的部分 (图源: Reasoning from Scratch)

优化后的版本先对完整的 prompt 做一次前向传播(prefill),之后每步只传入一个新 token,配合缓存的 K/V 完成 attention 计算:

Python — KV Cache 版本
from reasoning_from_scratch.qwen3 import KVCache

@torch.inference_mode()
def generate_text_basic_stream_cache(model, token_ids, max_new_tokens,
                                      eos_token_id=None):
    model.eval()
    cache = KVCache(n_layers=model.cfg["n_layers"])  # 初始化缓存
    model.reset_kv_cache()

    # Prefill:一次性处理整个 prompt,填充 cache
    out = model(token_ids, cache=cache)[:, -1]

    for _ in range(max_new_tokens):
        next_token = torch.argmax(out, dim=-1, keepdim=True)

        if eos_token_id is not None and torch.all(next_token == eos_token_id):
            break

        yield next_token
        # 只传入新生成的 1 个 token,cache 中已有历史 K/V
        out = model(next_token, cache=cache)[:, -1]

效果立竿见影:在 Mac Mini M4 CPU 上,从 5 tokens/sec 提升到 29 tokens/sec,接近 6 倍加速。原理很简单 — 避免了 O(n²) 的重复计算,每步的计算量从与序列长度成正比降为常数。


6. torch.compile 进一步提速

torch.compile 是 PyTorch 2.0+ 引入的 JIT 编译功能。它分析模型的计算图,自动进行算子融合、内存优化等,无需修改模型代码:

Python — torch.compile
# 一行代码开启编译优化
model_compiled = torch.compile(model)

# 首次运行会触发编译(较慢),后续运行大幅加速
# 使用方式和原模型完全一样
for token in generate_text_basic_stream_cache(
    model=model_compiled,
    token_ids=input_ids,
    max_new_tokens=100,
    eos_token_id=tokenizer.eos_token_id
):
    print(tokenizer.decode(token.squeeze(0).tolist()), end="", flush=True)

书中给出了不同硬件和优化组合下的性能对比:

模式 Mac M4 CPU Mac M4 GPU NVIDIA H100
基础版 5 tok/s 27 tok/s 51 tok/s
+ torch.compile 5 tok/s 43 tok/s 164 tok/s
+ KV Cache 29 tok/s 41 tok/s 48 tok/s
+ KV Cache + compile 68 tok/s 71 tok/s 141 tok/s

ℹ️ 性能解读:KV Cache 和 torch.compile 的收益可以叠加。在 CPU 上 KV Cache 效果最显著(6x 加速);在 GPU 上 torch.compile 效果更突出(因为它能优化 GPU kernel 调度)。两者结合是生产环境的标准做法。注意 H100 上 “KV Cache + compile” 比 “compile only” 略慢 — 这是因为 H100 的算力过剩,KV Cache 带来的内存操作反而成了瓶颈。


7. 关键收获

这一章虽然是 setup,但建立了几个贯穿全书的核心概念:

  • Tokenizer 是 LLM 的入口:所有文本必须先 encode 为 token ID 才能送入模型。Qwen3 的 BPE tokenizer 有 151,936 个 token,支持多语言。后续章节的 prompt 构造、奖励计算都依赖 tokenizer。
  • 自回归生成是基础范式generate_text_basic_stream 是后续所有采样策略(temperature、top-p、best-of-N)的基础框架。第4章会在这个函数上扩展采样参数。
  • KV Cache 是必备优化:不仅是推理加速的工具,第6-7章的 GRPO 训练中也需要生成多个 rollout 采样,KV Cache 直接影响训练效率。
  • Greedy decoding 的局限:本章用的 argmax 是确定性的贪心解码 — 同样的输入永远得到同样的输出。第4章会引入 temperature scaling 和 nucleus sampling 来增加多样性,这对推理模型至关重要。
  • Base model vs Reasoning model:Qwen3 0.6B base 已经能生成连贯文本,但它不会 “思考”。后续章节的目标就是通过各种技术(self-refinement、RLVR、蒸馏)让它学会推理。

ℹ️ 下一章预告:有了能生成文本的模型,下一步是如何评估它的推理能力。第3章将介绍 MATH-500 benchmark,实现答案提取和自动评分流程,建立贯穿全书的评估基准。

本文是 Build a Reasoning Model (From Scratch) (Sebastian Raschka) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。

Ch3: 评估推理模型 →

Related Posts