Qwen2.5 学习 Wiki
NOTES

从一句话到下一个 token:本科生版模型原理

大语言模型不是先在脑中写出整段答案,再把答案读出来。它每次只做一件事:**根据已经看到的 token,给词表中每一个候选 token 打一个分,选择下一个 token,然后把这个新 token 放回输入末尾,重复这个过程。**

Qwen2.5/07_Language_Model_Primer_CN.md 2026-07-13 约 13 分钟阅读

先用一句话说清楚

大语言模型不是先在脑中写出整段答案,再把答案读出来。它每次只做一件事:根据已经看到的 token,给词表中每一个候选 token 打一个分,选择下一个 token,然后把这个新 token 放回输入末尾,重复这个过程。

例如输入“北京是中国的”,模型可能给“首都”很高分、给“苹果”很低分。选出“首都”后,输入变为“北京是中国的首都”,接着继续预测下一个 token。

flowchart LR
    Text[用户文字] --> Tok[tokenizer]
    Tok --> IDs[token id 序列]
    IDs --> Forward[Transformer forward]
    Forward --> Scores[词表分数 logits]
    Scores --> Pick[top-k / sampling]
    Pick --> Next[下一个 token]
    Next --> IDs
    Next --> Decode[detokenize]
    Decode --> TextOut[输出文字]
流程或关系图。

1. token:模型看到的不是汉字,也不是 UTF-8 字节

人看到“你好”,会把它理解成两个汉字。模型处理的基本单位是 token。token 可能是一个字、一个子词、一个空格开头的英文片段,甚至是某些字节组合。

人的输入tokenizer 可能输出含义
hello[token_hello] 或多个子词 token高频英文片段通常会被合并。
Once upon a time多个 token id空格和单词边界都可能影响 token。
你好一个或多个 token id不是按“两个 Unicode 字符”固定切分。
ZCU106一个或多个 token id模型自己的词表决定切法。

项目中,真实词表和 merge 规则来自 GGUF 的 tokenizer.* metadata。export_gguf_tokenizer_assets.py 将它们导出;prepare_ps_tokenizer_bundle.py 再压成 A53 可读的二进制表;Q25TOKRT 把这些表放进一块可在 DDR 中打开的内存映像。

BPE 是什么

BPE 可以理解为“先从最小片段开始,再按词表规则把常一起出现的相邻片段合并”。它不是按空格简单切词。

flowchart LR
    Bytes[UTF-8 bytes] --> Initial[初始 byte token]
    Initial --> Pair[查相邻 token 对]
    Pair --> Merge{有更高优先级 merge?}
    Merge -->|有| Replace[合并为新 token]
    Replace --> Pair
    Merge -->|没有| IDs[token id 序列]
流程或关系图。

nl_uart_app.c 中,TEXT_PROMPT 有两条路径:打开 Q25TOKRT 后调用 q25_nl_tokenizer_bundle_encode_bpe_bounded;没有 image 时调用逐字节 fallback。后者能做 UTF-8 字节往返,但它不是模型真实 BPE 分词。

2. embedding:把 token id 换成数字向量

token id 只是一个整数,例如 12522。整数本身没有“北京”“首都”的含义。模型先在 embedding 表中查找这一行,把 id 变成一串浮点数,例如 896 个或 576 个数。这串数叫 hidden vector 或 hidden state。

可以把 embedding 表想成一本极厚的索引册:token id 是页码,页上的一行数是这个 token 在模型内部的起点表示。

概念数学形状在项目中的对应
token id一个整数NL_PROMPT_IDSTEXT_PROMPT_IDS
embedding tablevocab_size x hidden_size 矩阵GGUF token_embd.weight payload
hidden vector长度 hidden_size 的浮点数组token_embd_f32_lookuptoken_embd_q8_0_lookup 的输出

3. Transformer layer:让每个 token 看上下文,再做非线性变换

一个 Transformer layer 有两条主要计算支路:attention 和 MLP。两条支路都带 residual connection,也就是“把原输入加回来”。模型会重复许多层;Qwen2.5-0.5B 的 host 路径按 24 层运行,SmolLM2 路径有自己的层数和尺寸。

flowchart TD
    H0[上一层 hidden] --> N1[RMSNorm]
    N1 --> QKV[Q K V projections]
    QKV --> R[RoPE on Q K]
    R --> A[causal attention]
    A --> OP[attention output projection]
    OP --> Add1[加回 H0]
    Add1 --> N2[RMSNorm]
    N2 --> Gate[FFN gate]
    N2 --> Up[FFN up]
    Gate --> Act[SiLU gate * up]
    Up --> Act
    Act --> Down[FFN down]
    Down --> Add2[加回 Add1]
    Add2 --> H1[下一层 hidden]
流程或关系图。

RMSNorm:先把数字尺度拉回稳定范围

hidden vector 的数可能越传越大或越小。RMSNorm 用向量各元素平方的平均值来计算尺度,再用一组可训练权重调整每一维。它不是“把数据变成 0 到 1”,而是让不同 token 的向量长度处在比较稳定的范围。

项目中的 smollm2_qkv_rope_projectionsmollm2_mlp_full_layer 都先读取 residual 和 norm weight,再做 RMSNorm。

Q、K、V 和 attention:谁该关注谁

把一句话里的每个位置当作一个同学:

对当前 token 的 Q 与每个历史 K 做点积,得到 score;score 经 softmax 后成为权重;再按权重把所有 V 加起来,得到 context vector。

因果 mask 保证位置 t 只能看 0..t,看不到未来 token。否则训练时模型会偷看标准答案。

attention 步骤简化公式项目 kernel
生成 Q/K/VQ=XWq, K=XWk, V=XWvsmollm2_qkv_rope_projection
加位置信息对 Q/K 成对旋转同一 kernel 的 RoPE 部分
打分score=QK^T/sqrt(d)attention fixture 与核心实验
归一化p=softmax(mask(score))smollm2_attention_core
取上下文context=pVsmollm2_attention_core

RoPE:位置信息如何进入 Q/K

注意力的点积本身只会比较数值,不知道 token 位于第几个位置。RoPE 把 Q/K 向量中成对的元素按位置旋转不同角度。这样“第 3 个位置的词”和“第 30 个位置的词”即使内容相同,Q/K 也会不同。

MLP:每个位置单独做更复杂的特征变换

attention 负责跨位置收集信息;MLP 主要在每个位置自己的 hidden vector 上工作。项目中可见的形式是:

gate = normed_hidden x W_gate
up   = normed_hidden x W_up
mid  = SiLU(gate) * up
down = mid x W_down
output = residual + down

smollm2_mlp_full_layer 直接体现了这条链:Q5_0 的 gate/up,Q5_K 的 down,最后把 down 输出加回 residual。

4. KV cache:生成第二个 token 时不重复算历史 K/V

生成文本是逐 token 循环。第 100 次预测时,前 99 个 token 的 K/V 如果每次都重新计算,速度会很慢。KV cache 保存历史 token 的 K/V;新 token 只计算自己的 Q/K/V,然后用新 Q 读取历史 K/V。

sequenceDiagram
    participant Step1 as token 1
    participant Cache as KV cache
    participant Step2 as token 2
    Step1->>Cache: 写入 K1, V1
    Step2->>Cache: 读取 K1, V1
    Step2->>Cache: 写入 K2, V2
    Note over Step2,Cache: token 3 会读取 K1/V1/K2/V2
流程或关系图。

仓库存在 smollm2_kv_cache_handoff_20260629 等 HLS/fixture 工作,用来描述或试验缓存交接;PS UART 应用尚未实现完整的 KV cache 生命周期。

5. lmhead:从 hidden vector 回到“下一个 token 的候选分数”

Transformer 最后一层给出一个 hidden vector,但用户需要文字。lm_head 是一个很大的矩阵:用 final hidden vector 和词表中每一行做点积,得到每个 token 的 logit。

flowchart LR
    H[final hidden vector] --> Dot[与每个 lm_head 行做 dot]
    Dot --> L[每个 token 的 logit]
    L --> Sort[top-k 或 sampling]
    Sort --> ID[选定 next token id]
    ID --> Text[tokenizer 解码]
流程或关系图。

logit 不是概率。softmax 把一组 logits 变成总和为 1 的概率;greedy decoding 直接选最大 logit;top-k 只保留最高的 K 个候选再按策略选一个。smollm2_lm_head_streaming_topkq25rt_i8_lm_head_paged 都是“少量行/页扫描并保留 top-k”的硬件算子,而不是完整对话系统。

6. 量化:用更少的位存权重

原始模型权重通常用 F32 浮点数,一个数占 4 字节。模型很大,放进 DDR、读取到 PL、做矩阵乘法都会受内存容量和带宽限制。量化把一组权重存成较少的整数位和少量尺度参数。

格式一个 block 覆盖block 字节直觉
F321 个数4最直接、体积最大。
Q8_032 个数3432 个 int8 加一个尺度。
Q5_032 个数22每个数约 5 bit,加尺度和高位打包。
Q5_K256 个数176K-quant,分块尺度/最小值更复杂。
Q4_K256 个数144更紧凑,精度与解码复杂度需要同时考虑。
Q6_K256 个数210约 6 bit 的 K-quant。

量化不是简单的“把浮点数强行取整”。正确计算必须按 GGML block 布局读取 scale、min、低位和高位,再反量化或直接进行量化点积。项目的 layer0_q6k_down_projection 读取 Q6_K block;smollm2_mlp_full_layer 解码 Q5_0/Q5_K;Host golden 负责提供同一输入下的参考数值。

7. 本项目的完整映射

模型概念你可以先看哪个模块该模块给你的答案
token 和 BPEmodules/001_model_assets_and_tokenizer词表、merge、Q25TOKRT 和 C API 如何工作。
GGUF 与量化 tensormodules/002_host_reference_pipeline原始权重如何导出、反量化和生成 golden。
Transformer 的硬件子算子modules/003_hls_pl_kernelsQKV、attention、MLP、lm_head 各自的 DDR/AXI 接口。
板上文字命令modules/004_ps_uart_runtimeUART 命令怎样进入 tokenizer 或 P4 stub。
真正上板的流程modules/005_board_executionELF、串口、transcript、归档如何连起来。
如何判断一个文件表示什么modules/006_artifact_validationmanifest、hash、fixture 和日志如何阅读。

8. 读这个项目时最重要的区分

1

模型算法:Transformer 应该怎样从 token 得到 logits。

2

Host 参考:PC 上用同一份量化数据算出参考数值。

3

HLS 单算子:硬件只实现或验证算法中的一个片段。

4

PS/UART 命令:板上应用目前负责命令和 tokenizer,不等于完整模型 forward。

5

板级运行:ELF 和串口能运行,说明板上软件链路存在;是否运行完整模型要看它实际调用了哪些模块。

把这五件事分开,就不会把“某个 kernel 有 CSim”或“UART 能回复文本”误读为“Qwen2.5 完整模型已经在板上生成答案”。