首页/目录/全部文章

全部文章

八个专题的源码、算法与协议笔记都在这里。

笔记列表

GGML:内存分配、量化体系与 GGUF

GGML:内存分配、量化体系与 GGUF

「本系列第 3/17 章」

上一章说明算子只建图、ggml_build_forward_expand 只排序。图一旦可执行,立刻出现两个工程问题:中间激活往哪放,以及磁盘上的量化权重如何变成可点积的 block。本章把内存分配、量化体系和 GGUF 放在一起讲,因为 llama.cpp 加载模型时三条线同时发生。

两套内存,各管各的

体系 机制 装什么
Context bump allocator 张量 / 图 元数据
Backend gallocr + dyn_tallocr 张量 data

Context 在 ggml_init 时一次性拿到一块 CPU 内存,按对象顺序往前推。它不知道 GPU VRAM,也不给 7B 权重当堆。权重与激活的真实字节在 Backend buffer 里。ggml_tallocr 是单 buffer 线性 bump,适合按文件顺序把权重量进去、分配后不再 free。中间激活则走 ggml_dyn_tallocr:best-fit 空闲块,最多 256 个 free block,节点算完可以归还。gallocr 是图级指挥:为每个 Backend buffer type 持有一套 dyn_tallocr,按拓扑给节点发地址。

权重加载用 ggml_backend_alloc_ctx_tensors_from_buft,一次把 Context 里所有权重量到指定 buffer。这些地址不参与 decode 过程中的 free。激活才走 ggml_gallocr_alloc_graph。llama.cpp 初始化时先对 worst-case 图做 reserve,避免每步 token 都 realloc。

何时原地写,何时绝不覆盖

gallocr 按拓扑遍历。对每个节点:若是 view,则共享父地址加偏移;若满足 in-place,则复用父 buffer;否则向 dyn_tallocr 要一块新的。节点「完成」后把依赖它的 n_children 减一,减到 0 且不是输出,就把块还回去。

in-place 同时要求四件事:ggml_op_can_inplace 为真(ADD、MUL、SCALE、SOFT_MAX、ROPE、RMS_NORM、UNARY、GLU 等);父节点 n_children==1;同一个 buffer_id;父尺寸不小于子尺寸。少一条都不能复用。上一章的 use_counts 与这里的 n_children 是同一事实的两面:一个张量被两个下游读,就绝不能让第一个下游原地写爆。

GGML_TENSOR_FLAG_OUTPUT 是硬约束:永不覆盖、永不提前 free。logits、需要回读的 hidden、要留给采样器的最后一层,都必须打这个标志。外部已经设置 tensor->datatensor->buffer 的张量同样不参与回收。漏打 OUTPUT,再叠加 pipeline 的 n_copies 轮转,就会出现「偶发错词、复现不稳定」——那不是采样温度的问题,是内存别名。

峰值因此远小于「层数 × 每层激活」。十层 FFN 若每层中间 1GB,无复用要 10GB;拓扑允许时往往只要约两层的量级。这是 GGML 能在消费级显存上跑 7B/14B 的前提之一,另一半来自量化。

量化:热路径是点积,不是反量化

GGML 的量化不是「先 decode 成 F32 再调用 BLAS」。生产路径是:

1
2
3
4
5
GGUF 中的 Q4 权重
→ [可选] GGML_CPU_REPACK 改 layout
→ 激活在线量化为 Q8
→ vec_dot(Q4 权重, Q8 激活)
→ F32 累加输出

标准 block 以 32 元素为单位,例如 block_q4_0 带一个 half 尺度和 16 字节 nibble。K-quant 把 super-block 放到 256,尺度分层,压缩率与误差都更好。Q4_K_M 是目前最常见的生产 ftype:体积大约 4.5–5 bit/weight 量级,质量明显稳过老的 Q4_0,又比 Q8_0 省一半以上空间。

激活走 Q8,是因为 8bit 点积在 AVX-VNNI、NEON dotprod、CUDA DP4A / tensor core 路径上都有现成吞吐,而权重保持 4bit 负责省带宽。CPU 上函数形态是 ggml_vec_dot_q4_K_q8_K 这类组合;CUDA 上小形状走 mmvq,批量量化矩阵走 mmq,浮点批量才落到 cublas。不要在热路径里对整层调用 dequantize_row_*——那些函数给 llama-quantize、调试和个别不支持的 op 用。

IQ 系列(IQ1/IQ2/IQ3/IQ4)用 codebook,而不是均匀网格。ggml_quantize_requires_imatrix 对它们为真:必须先用校准文本跑重要性矩阵,再按列加权选码。没有 imatrix 的 IQ 文件可以写出,但生成质量会差到失去「更低 bit」的意义。K-quant 也能吃 imatrix,但不是强制。

GGML_CPU_REPACK 默认打开。权重从 GGUF 读入后,可能再变成对 SIMD 更友好的 layout(例如 Q4_X)。这是加载期一次性 CPU 开销,换运行期 vec_dot 更快。它不改变 GGUF 文件本身,只改变 Backend buffer 里的排列。

GGUF:自描述容器,而不是「裸权重 dump」

GGUF 的文件头很短,语义却完整:

1
2
3
4
magic "GGUF" | version=3 | n_tensors | n_kv
→ metadata KV
→ 每个张量的 name / n_dims / dims / type / offset
→ 按 32 字节对齐的 data blob

GGUF_MAGIC 是四个字符 GGUFGGUF_VERSION 为 3,GGUF_DEFAULT_ALIGNMENT 为 32。更高版本直接拒绝;alignment 可被 general.alignment 覆盖,offset 必须按它计算,否则后面所有张量都会错位。

KV 至少要能回答:架构名(general.architecture)、文件量化类型(general.file_type)、量化版本、上下文长度、隐层宽度、层数、Q/KV 头数、词表与 bos/eos。张量名遵循 llama.cpp 的约定:token_embd.weightblk.{i}.attn_q.weightblk.{i}.ffn_gate.weightoutput_norm.weight。转换脚本写错名字,加载器会建出缺边的图。

llama.cpp 如何打开一个文件

关键调用是 gguf_init_from_file,参数里 no_alloc=true

1
2
3
struct ggml_context * ctx = NULL;
struct gguf_init_params params = { .no_alloc = true, .ctx = &ctx };
struct gguf_context * meta = gguf_init_from_file("model.gguf", params);

四步缺一不可:

  1. 解析 KV,填 hparams。
  2. 为每个张量创建 只有元数据ggml_tensordata 仍为空。
  3. ggml_backend_alloc_ctx_tensors_from_buft 按设备分配权重 buffer。
  4. offset 把 data blob 读进对应 buffer;CPU 上再视情况 repack。

这解释了为什么同一份 Q4_K_M 既能全 CPU 跑,也能 -ngl 上 GPU:文件不包含设备信息,设备是加载期选的。也解释了为什么 Context 只要十几 MB:7B 的 Q4 权重从来不进 bump 池。

写入路径是对称的。convert_hf_to_gguf.pygguf-py 写 F16/F32 GGUF;llama-quantizegguf_init_from_file 读入,逐张量 dequant → quantize_*gguf_write_to_file。量化发生在离线工具里,推理进程只做 vec_dot

把三条线收束到一次 decode

一次 decode 的内存画面是:权重量在 Backend buffer 里,生命周期等于模型;input token 与 RoPE 位置是 INPUT;中间 RMSNorm / SILU / 残差在 n_children==1 时原地写;最后 logits 是 OUTPUT。量化只影响权重与激活的 type,不影响这套生命周期。GGUF 只在加载与导出时出现,不在逐 token 热路径上再解析一遍 KV。

排查时按层提问。OOM 且发生在加载:看量化类型和 alloc_ctx_tensors 的设备。OOM 且发生在运行:看 gallocr_reserve 是否按最大 batch / 最长上下文预留。结果错但 loss 在训练侧正常:先核对 GGUF 的 architecture 与 tokenizer,再核对是不是 OUTPUT 被覆盖。IQ 模型「能跑但很蠢」:先问 imatrix 在不在。

从训练产物走到可推理文件

把本章三块机制嵌回总览里的转换桥,顺序就固定了。LLaMA-Factory 写出 HF 目录后,convert_hf_to_gguf.py 只负责翻译:架构名写成 general.architecture,词表写成 tokenizer.ggml.tokens,每个 SafeTensors 矩阵变成带 offset 的 GGUF 张量,默认对齐 32 字节。这一步通常输出 F16 或 F32,体积接近原模型,目的是留下一份「尚未损伤、但已经自描述」的中间文件。

llama-quantize 读这份中间文件时同样 no_alloc 思路:先看元数据,再按目标 ggml_type 逐行 quantize_chunk。目标是 Q4_K_M 时走 K-quant;目标是 IQ 时必须传入 imatrix。写出的新 GGUF 换了 general.file_type 和每张量的 type,但名字与 dims 应保持不变。推理进程加载新文件时,Context 仍然只持有元数据,gallocr 仍然只为激活工作,变化仅仅是 vec_dot 选了另一对类型组合。

因此「量化」不是 llama-server 启动后的一个选项,而是文件已经写死的 type。server 能做的是:把已量化权重放到 CUDA 或 Metal buffer,在运行期把激活收成 Q8,以及决定要不要 CPU repack。它不能把一份 Q4_K 文件「临时当成 Q8」而不经过重新量化工具。分清离线量化与在线激活量化,才能理解为什么同一套 GGUF 在 CPU 与 GPU 上数值应接近,而 HF 的 bitsandbytes 量化不能直接改后缀当 GGUF 用。

再补一条对齐相关的实务:32 字节不是装饰。GPU alloc_size 往往比 ggml_nbytes 更大,为的是满足 buffer type 的 alignment。手写解析器若按「张量字节数简单相加」算下一个 offset,会在中途偏离 data blob,后面所有层都错。用官方 gguf_init_from_file 的原因正在于此:KV、对齐、version=3 的拒绝逻辑已经写死,不必自己再实现一遍容器。分片文件只是把同一套规则切成多个 blob,加载器合并权重表之后,对 gallocrvec_dot 仍然是一张完整的模型图,并不需要改变 in-place 条件或 OUTPUT 标志。

下一章讨论 GGML 的 Backend 抽象与硬件加速。

GGML:Backend 抽象与硬件加速

GGML:Backend 抽象与硬件加速

「本系列第 4/17 章」

前两章把图建成了,也把权重与激活放进了 buffer。还缺一层:谁来声明「我能算这个 op」,谁来把一张图切到 CPU 与 GPU 上。GGML 0.15.3 的答案是五层 Backend vtable,加上注册表、三遍调度和按硬件手写的矩阵核。llama.cpp 的生产执行入口是 ggml_backend_sched_graph_compute_async

五层 vtable,而不是「一个 GPU 句柄」

ggml-backend-impl.h 把设备能力拆开,避免把「内存什么样」和「图怎么跑」焊死在同一张表上:

职责
ggml_backend_buffer_type_i 对齐、是否 host、alloc_size(可含 GPU padding)
ggml_backend_buffer_i alloc/free、init_tensor、同步拷贝
ggml_backend_i graph_computesynchronize、异步 tensor / event
ggml_backend_device_i supports_opsupports_buftoffload_op
ggml_backend_reg_i 注册名、枚举 device、proc_address

调度器问的是 device:「这个节点你能不能算、这个 buffer type 你能不能碰」。真正开火的是 backend 的 graph_compute。buffer type 决定权重该申请 host 还是 VRAM,以及 ggml_backend_buffer_get_alloc_size 要比 ggml_nbytes 多出多少对齐。把五层合成「cudaMalloc 加一个 kernel 表」,后面的切图与 pipeline 都解释不清。

Buffer 还有用途标记。WEIGHTS 告诉调度器:能在哪块 buffer 上算,就尽量让计算跟权重走,减少把 Q4 权重传来传去。COMPUTE 标记中间激活,可以安心放在 GPU。ANY 没有倾向。llama.cpp 给权重量 WEIGHTS,是混合 offload 能工作的前提。

注册顺序决定默认优先级

ggml-backend-reg.cpp 的静态顺序是:CUDA → Metal → SYCL → Vulkan → … → CPU(最后)。调度器数组里下标越小优先级越高。CPU 最后注册,不是因为慢就弃用,而是它 supports_op 对全部算子返回真,必须当兜底。某节点 GPU 不支持——非连续、白名单外的 UNARY 子类型、split buffer 上的非 MUL_MAT——就会落到 CPU,并插入跨设备 copy。

运行时仍可 GGML_DISABLE_VULKAN=1 关掉 Vulkan,或用 ggml_backend_init_by_name("CUDA", NULL) / ggml_backend_init_best() 显式选设备。动态加载(GGML_BACKEND_DL)把 libggml-cuda.so 一类 MODULE 推进同一张表,顺序规则不变。编译了 CUDA 却看见所有 MUL_MAT 在 CPU,先查注册表里有没有 CUDA device,再查权重 buffer 是不是 host。

调度器三遍切图,pipeline 四个副本

ggml_backend_sched_split_graph 对已展开的 cgraph 做三遍,而不是按层号硬切:

1
2
3
4
5
6
7
8
Pass 1  已有 buffer 的 leaf/node 绑定 Backend
权重在 GPU → GPU;INPUT 通常绑最后一个 Backend(CPU)

Pass 2 从已绑定节点向邻居扩张到更高优先级 Backend
除非权重就在 CPU,否则尽量离开 CPU

Pass 3 剩余节点看 supports_op 与 supports_buft
不兼容就切开 split,并插入 copy 节点

切完得到 splits[]compute_splits 对每个片段:先把依赖张量拷到目标设备,再调用该 Backend 的 graph_computen_copies 默认为 4(GGML_SCHED_MAX_COPIES),用多份 buffer 轮转,让 H2D 与计算重叠。这就是 async 入口必须在复用 input 之前 synchronize 的原因:四个副本用完会回到第一份,上一轮若未结束就会被下一轮 token 覆盖。

MoE 有额外收紧:MUL_MAT_ID 只拷用到的 expert slice,不把整块专家权重在设备间搬。普通 dense 层没有这条优化,offload 策略更依赖「权重一开始就放对设备」。

和三条执行路径的关系可以收成一句:ggml_graph_compute 不经过这套切分;ggml_backend_graph_compute 只有一个 Backend,不必 split;只有 ggml_backend_sched_graph_compute_async 跑满三遍加 copies。llama.cpp 选第三条,因为用户常常 -ngl 只卸一部分层,CPU 与 GPU 必须共存。

CUDA:mmvq、mmq、cublas 三条矩阵路

CUDA 后端与 HIP / MUSA 共用 ggml-cuda/ 源码树,注册名不同。MUL_MATggml-cuda.cu 里按形状分流:

条件 走哪
二维且是向量情况,量化 mmvqggml_cuda_mul_mat_vec_q
二维且是向量情况,浮点 mmvf
其余量化矩阵 mmqggml_cuda_mul_mat_q
其余浮点批量 cublas

decode 一步、batch=1 时,Q 投影往往是矩阵×向量,mmvq 比 cublas 启动更便宜。prefill 长序列则 mmq / cublas 吃得更饱。GGML_CUDA_FORCE_MMQGGML_CUDA_FORCE_CUBLAS 只用于对拍,不是默认生产开关。

Flash Attention 由 GGML_CUDA_FA 控制,默认打开;GGML_CUDA_FA_ALL_QUANTS 才会为全部量化类型编 FA kernel,二进制会胀。split buffer 多卡模式只允许 MUL_MAT / MUL_MAT_ID 跨卡切行,其它 op 必须整段待在一张卡上。这是 supports_op 里写死的限制,不是调度器疏忽。

Metal 与 Vulkan:默认路径和生成路径

macOS 上 Metal 默认 ON。Apple Silicon 统一内存让 Shared buffer 成为常见选择;offload_op 仍可能拒绝过小的 batch,避免 launch 比计算还贵。MSL kernel 集中在 ggml-metal.metal,量化 matmul、RoPE、Flash Attention 都在同一套 shader 里,而不是调用 cuBLAS 的等价物。

Vulkan 面向跨平台 GPU。vulkan-shaders-genglslc.comp 编成 SPIR-V,再按约 25 种量化 / 浮点类型做笛卡尔积,嵌入 ggml-vulkan-shaders.hpp。所以打开 GGML_VULKAN 时,配置阶段会先跑 shader-gen;关掉它或设 GGML_DISABLE_VULKAN=1,注册表里就不会出现 Vulkan device,调度器自然不会把节点切过去。

三家 GPU 对上一章的 Q4×Q8 约定是一致的:CPU 用 SIMD vec_dot,CUDA 用 mmvq/mmq,Metal / Vulkan 用各自的 quant kernel。改变量化类型意味着四套核都要能认 ggml_type,这也是新类型只能追加在 enum 末尾的原因。

构建开关如何变成运行时能力

常用 CMake 不要一次全开,按机器选:

选项 作用
GGML_CUDA 编译 CUDA 后端(HIP/MUSA 另开,但共用源码)
GGML_CPU_REPACK 加载期重排 Q4 等 layout,默认 ON
GGML_CUDA_FA CUDA Flash Attention
GGML_METAL Apple GPU,macOS 默认
GGML_VULKAN 跨平台 GPU + shader-gen
GGML_SCHED_MAX_COPIES pipeline 副本,默认 4

GGML_CPU_REPACK 不影响「有没有 GPU」,只影响 CPU 权重量进 buffer 之后的排列。GGML_CUDA 没开时,注册表没有 CUDA,sched 再聪明也只能 CPU。GGML_CUDA_FA 关掉则 Flash Attention 节点更容易 supports_op 失败,被切回 CPU,表现为 prefill 突然变慢而 decode 还过得去。

一次生产调用怎么落到硬件

1
2
3
4
5
6
7
8
9
10
11
12
ggml_backend_reg 选出 CUDA / Metal / CPU
ggml_backend_sched_new(backends…) # 下标 0 优先级最高
权重量到带 WEIGHTS 的 GPU buffer
每步 decode:
sched_reset
建图 + ggml_build_forward_expand
sched_alloc_graph → gallocr
sched_graph_compute_async
split_graph 三遍
compute_splits(copy + graph_compute)
sched_synchronize
读 OUTPUT

对照本栈:LLaMA-Factory 与 LlamaIndex 都不出现在这张表里。转换桥只决定 type 是 Q4_K 还是 Q8_0;Backend 决定 Q4_K 的 vec_dot 跑在 mmq 还是 AVX。你在 llama-server 设的 -ngl、设备列表,最终就是这里 backends[] 与权重 buffer_id 的组合。

排错按层缩小。全在 CPU:注册与 CMake。部分层在 CPU:该 op 的 supports_op 或非连续 layout。跨设备 copy 占满时间线:权重没标 WEIGHTS,或 INPUT 每步从 CPU 推过大张量。结果偶发损坏:先 synchronize,再确认 OUTPUT 与 n_copies 轮转。这三章构成 GGML 的最小闭环;下一章开始,轮到 llama.cpp 用这些 API 加载模型并建出整网推理图。

硬件选择怎样映射到本栈

本系列后半会把 llama-server 暴露给 LlamaIndex,但加速决策必须在 GGML 这一层做完。一台只有 NVIDIA 的 Linux 机器:打开 GGML_CUDA,确认注册表第一位是 CUDA,权重量到 GPU 的 WEIGHTS buffer,decode 走 mmvq,prefill 走 mmq。一台 Mac:不要强行上 Vulkan 当默认,Metal 已经默认编译;统一内存上 Shared buffer 减少显式 H2D,但小 batch 仍可能被 offload_op 留在 CPU。只有跨厂商 GPU、又没有 CUDA 工具链时,才把 GGML_VULKAN 和 shader-gen 当作主路径。

CPU 永远在最后当兜底,所以「能跑」不等于「在加速」。真正要检查的是切分后有多少 MUL_MAT 落在 CPU split 里。若几乎全部矩阵乘都在 CPU,再快的 Q4 vec_dot 也只是 SIMD 优化,不是 GPU 吞吐。GGML_CPU_REPACK 在这种退化路径上反而更有价值:它让 CPU 自己的点积更快,但不能替代你把 CUDA 编进二进制。

和量化一章连起来看:硬件不改变 Q4 权重 × Q8 激活这条数学约定,只改变 kernel。和张量一章连起来看:调度器再切,也只能切 build_forward_expand 之后的节点数组;建图阶段选错 op,没有 Backend 能补救。和总览连起来看:LLaMA-Factory 的显卡利用率属于 PyTorch 训练,llama-server 的显卡利用率属于本节的 Backend;两者不是同一块显存池,不要在训练进程还占着 GPU 时,指望推理调度器还能把 7B 的 Q4_K 全部卸上去。

读完 GGML 三章,你应当能在纸上画出:惰性图如何展开,gallocr 如何按 n_children 回收,GGUF 如何 no_alloc 加载,以及 sched 如何三遍把节点送给 CUDA / Metal / CPU。后面的 llama.cpp 章节不再重复这些机制,只会指出它调用了哪一个入口。若你现在就去启动 llama-server,先确认二进制里确实链上了 ggml-cudaggml-metal,否则注册表只剩最后的 CPU 兜底,后面所有 RAG 延迟都会被误判成 LlamaIndex 检索慢,而真正慢的是矩阵乘从未离开主机内存。

下一章进入 llama.cpp:它如何把 GGUF 变成可执行的推理图。

llama.cpp:编译、工具链与 GGUF 生态

llama.cpp:编译、工具链与 GGUF 生态

「本系列第 5/17 章」。前四章已经把 GGML 的张量、量化、GGUF 与 Backend 收成最小闭环。从本章起进入 llama.cpp:先把源码编成可复制的二进制,并把 Hugging Face 权重变成引擎能加载的 GGUF。

1. 先认产物

一次完整构建,至少要能指认出五件东西:

产物 路径 / 头文件 职责
libllama src/ + include/llama.h 模型、context、decode、采样的 C API
libggml ggml/ 张量、计算图、量化、多 Backend
llama-cli tools/cli/ 命令行推理与对话
llama-server tools/server/ OpenAI 兼容 HTTP
llama-quantize tools/quantize/ 把 F16 GGUF 压成 Q4_K_M 等

工具都链到 libllamalibllama 再链 libggml。调试「编过了却跑不起来」时,先分清是库没编出来、模型文件不对,还是运行时缺 .so。默认动态库构建要把 build/bin 加进 LD_LIBRARY_PATH;虚拟机或跨机拷贝更适合静态链接。

2. 一份可移植的 CMake

目标如果是虚拟机,或要把二进制拷到另一台 x86_64 机器,不要用本机最优指令集:

1
2
3
4
5
6
7
8
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DGGML_NATIVE=OFF \
-DGGML_AVX2=ON \
-DGGML_CUDA=OFF \
-DLLAMA_BUILD_UI=ON
cmake --build build --config Release -j$(nproc) \
--target llama-cli llama-server llama-quantize

记住这几条开关,而不是背一整页 CMake:

  • BUILD_SHARED_LIBS=OFF:核心库静态链进可执行文件,复制时少拖一堆 .so
  • GGML_NATIVE=OFF:关掉 -march=native,避免在另一台 CPU 上遇到 Illegal instruction
  • GGML_AVX2:给通用 x86_64 一个稳妥的 SIMD 下限;更老的机器再退到 GGML_SSE42=ON 并关掉 AVX
  • GGML_CUDA:有 NVIDIA GPU 再打开,运行时配合 -ngl 99;本章冒烟可以先关
  • LLAMA_BUILD_UI:把 Web UI 嵌进 llama-server

只开 LLAMA_BUILD_UI 不够。前端资源必须先落到 tools/ui/dist(至少要有 index.html)。没有这份目录,服务端仍可能编过,浏览器打开却是空壳。先放好 dist,再编 llama-server。Apple 上 Metal 通常默认打开;Linux 上 Vulkan / SYCL 都是显式 CMake 选项。

3. GGUF 是一条单向流水线

llama.cpp 不直接吃 PyTorch / SafeTensors。标准路径是:

1
2
3
4
5
HuggingFace 模型
→ convert_hf_to_gguf.py
→ F16 GGUF
→ llama-imatrix # IQ / 高质量 K-quant 建议走
→ llama-quantize Q4_K_M

convert_hf_to_gguf.py 负责架构映射、词表和 metadata,先产出 F16 GGUF,作为保真的中间态。conversion/ 里按 LlamaForCausalLMQwen2ForCausalLM 等名字注册转换器。漏掉 bos / eos 或把 general.architecture 写错,后面文件能打开,建图会对不上层数或头数。

llama-imatrix 在校准文本上统计激活重要性,得到 imatrix.dat,再交给 llama-quantizeQ4_K_M 是生产默认;IQ 系列强制 imatrix,跳过会明显掉质量。量化发生在 ggml-quants.c 的参考实现里,不走 CUDA mmq。分片模型的两个 .gguf 必须放在同一目录,-m 只指向第一片。

4. 冒烟:先跑通,再谈优化

日常验证不要一上来就上大模型。推荐 Qwen2.5-0.5B-Instruct-GGUFq4_k_m:体积大约四百多 MB,中文可用,内存友好。不要用 llama-cli -hf 在弱网上边下边起服务,网络卡住时几乎没有日志。更稳的做法是 HF_ENDPOINT=https://hf-mirror.com hf download 先把文件落到磁盘。

1
2
3
4
./build/bin/llama-cli -m qwen2.5-0.5b-instruct-q4_k_m.gguf \
-co -cnv -p "You are Qwen..." -t 4 -n 64
./build/bin/llama-server -m qwen2.5-0.5b-instruct-q4_k_m.gguf \
--host 0.0.0.0 --port 8080 -t 4 --parallel 4

llama-cli 证明库和模型都对;llama-server 必须等到日志出现 HTTP server is listening 再打 /health/v1/models/v1/chat/completions。吞吐可以用 llama-bench -m ... -t 4 -p 128 -n 64 看 prefill 与 decode。工具箱里还有 llama-tokenizellama-gguf-splitllama-mtmd-cli,但闭环只要求 cli + server + quantize 三件套。

下一章《llama.cpp:推理原理与 C API》会从「能跑」走到「每次调用在做什么」:自回归、prefill / decode,以及 llama_modelllama_context 的分工。

llama.cpp:推理原理与 C API

llama.cpp:推理原理与 C API

「本系列第 6/17 章」。上一章已经能编出 llama-cli / llama-server,并准备好一份 GGUF。本章把「输入文本变成下一段文本」收成一条 C API 心智模型:对外只认 include/llama.h,对内则是只读权重加一份不可共享的会话状态。

1. 自回归:每次只多一个 token

语言模型推理是自回归的:给定前缀,预测下一个 token,追加后再预测。用户看到的「逐字往外吐」,就是这个循环。Causal Attention 规定位置 i 只能看见 ≤ i;训练时可以对整段序列并行算 loss,推理时通常只要最后一个位置的 logits。没有 KV cache 时,每一步都要把前文重算一遍,复杂度按序列长度平方涨;有 cache 之后,decode 只算新 token 的 K/V,并读取历史。

2. Prefill 与 Decode 是同一扇门

工程上常把推理分成两段,但 llama.cpp 没有两套入口。Prompt 一次性送进模型叫 prefill:token 多、可并行、主要职责是填满 KV,通常不为中间位置采样。之后每次只送一个新 token,叫 decode:计算量小、延迟敏感,每步调用一次 llama_sampler_sample

两端都调用 llama_decode。区别只在 llama_batch 的大小,以及 logits 标志。llama_batch_allocr 默认只给最后一个 token 打开 logits。把 prefill 理解成「第一次 decode」,后面的循环就顺了。首 token 延迟主要由 prefill 决定,用户感知的吐字速度才是 decode。

3. 两个对象,两种线程语义

可以把对象关系记成:llama_model 像只读程序,llama_context 像进程,llama_batch 是本次输入包。

llama_model 装着权重、词表和架构,加载后只读,可被多个会话共享。一份 GGUF 不必为每个用户复制一遍。llama_context 装着一次推理的运行时:llama_memory_tggml_backend_sched、logits / embedding 缓冲。llama_context 不是线程安全的。多线程服务应当一线程一份 context,或在外层串行化。llama_backend_init / llama_backend_free 同样不是线程安全的,进程里只做一次。

llama_decode 的返回值要当成控制流:

返回值 含义
0 成功
1 KV 已满,当前 batch 放不进去(可 defrag 后重试)
-1 参数错误
-2 内部错误(图分配或计算失败)
2 用户 abort

返回 1 时可以清序列、缩短上下文或增大 n_ctx;返回 -2 则应停止,不要继续采样。

4. 最小 C 循环

加载仍是两阶段:gguf_init_from_file(no_alloc=true)llama_model_loaderllama_model_create。对外则收成:

1
2
3
4
5
6
7
llama_backend_init();
llama_model * model = llama_model_load_from_file(path, llama_model_default_params());
llama_context * ctx = llama_init_from_model(model, llama_context_default_params());
/* tokenize → llama_decode(prefill) → sample → llama_decode(one token) → ... */
llama_free(ctx);
llama_model_free(model);
llama_backend_free();

对照 examples/simple/simple.cppllama_model_params 里最常改的是 n_gpu_layersuse_mmapsplit_modellama_context_params 里是 n_ctxn_batchn_ubatchn_seq_maxn_threadsflash_attn_typetype_k / type_v。采样不要手写 argmax 凑合:用 llama_sampler_chain_init 串 greedy / top_k / top_p / temp / penalties / grammar。Embedding 模型把 pooling_type 设成 LLAMA_POOLING_TYPE_MEAN,再读 llama_get_embeddings_seq。LoRA 走 llama_adapter_lora_init + llama_set_adapter_lora

KV 显存可以先按 2 × n_layer × n_ctx × n_head × head_dim × sizeof(dtype) 估算。7B、4096 上下文、FP16 的 K/V 往往就要 1–2 GB,这还没算权重。--cache-type-k q8_0-fa on 是降 KV 与算子开销的第一组旋钮。

下一章《llama.cpp:Decode、KV Memory 与 Graph 复用》会打开 llama_decode 的内部:batch 如何被切开,Memory 如何按架构选型,以及为何逐步生成可以复用同一张计算图。

llama.cpp:Decode、KV Memory 与 Graph 复用

llama.cpp:Decode、KV Memory 与 Graph 复用

「本系列第 7/17 章」。上一章把 llama_decode 当成黑盒:送进 batch,得到 0 / 1 / -2。本章沿同一条调用往下走,看一次 decode 如何切 batch、如何申请历史槽位、如何决定「重建图还是复用图」。读完应能把「KV 满了」和「这一步突然变慢」对应到具体阶段。

1. 一次 decode 的主轴

llama_decode 进入 llama_context::decode() 之后,主路径可以收成:

1
2
3
4
5
6
balloc.init
→ sched_reserve / memory_update
→ memory.init_batch
→ loop process_ubatch
apply / can_reuse / build_graph / graph_compute
→ output_reorder

balloc.init 先给用户提交的 llama_batch 做消毒:补全缺失的位置、序列号和 logits 标志。位置通常从该序列 seq_pos_max + 1 递增,logits 默认只打开最后一个 token。随后 memory.init_batch 在历史存储里为这批 token 找槽位;找不到就 memory_update(true) 做 defrag / shift,再失败才返回 1。槽位就绪后进入 process_ubatchapply 把分配写入 cells,再判断旧图能否复用;不能复用则 model.build_graph() + sched_alloc_graph(),最后 graph_compute。计算失败会 seq_rm 回滚刚写入的 KV,避免 cache 与图各走各的。

build_graph 本身也分层:build_arch_graphsrc/models/*.cpp 的三方法之一)→ build_poolingbuild_samplingbuild_dense_outset_outputs。新架构只要实现 load_arch_hparamsload_arch_tensorsbuild_graph,并在 llama-arch.h / llama-model.cpp 注册。

2. Batch 为什么要切成 ubatch

公开结构是 llama_batchtoken[]embd[] 二选一,再加上 posseq_idlogits。它不会直接变成计算图输入,而是经过 llama_batch_allocr,拆成更小的 llama_ubatchn_batch 是逻辑上限,n_ubatch 是单次 graph_compute 上限,且必须 n_ubatch <= n_batch。一次 prefill 可能有数百个 token,必须切开,否则峰值激活会先于权重把显存撑爆。

三种拆法对应三种存储形态:

  • split_simple:按 token 顺序切,单 stream KV 最常见
  • split_equal:各序列对齐取 token,多 stream 时用来保证 attention 对齐
  • split_seq:一次只推进一个序列集合,循环模型需要这种节奏

标准 KV 在 n_stream==1 时用 split_simple,多 stream 用 split_equal。非因果 attention 要求整个 batch 落在一个 ubatch 里。后端采样还限制「每条序列最多一个 output token」。LLAMA_BATCH_DEBUG=1 可以打印拆分结果。

3. Memory 工厂:一种接口,五类实现

历史状态并不总是「标准 KV cache」。llama_memory_i 统一 init_batchseq_rm / seq_cpstate_writellama_model::create_memory() 按架构分流:

  • BERT / Embedding 等 encode-only:返回 nullptrllama_decode 回退到 encode,输出向量
  • DeepSeek32:llama_kv_cache_dsa(MLA key + Lightning Indexer)
  • Mamba / RWKV:llama_memory_recurrent,定长循环状态,batch 用 split_seq
  • Jamba / Qwen3.5:llama_memory_hybrid,注意力层 KV,循环层 recurrent
  • 其余:llama_kv_cache;带滑动窗口则 llama_kv_cache_iswa(全上下文 + SWA 双实例)

标准 KV 的 prepare 是事务性的:先 find_slot(dry_run=true) 环形搜索连续空槽,全成功才 commit。server 的 slot fork 底层就是 llama_kv_cache_seq_cp。Qwen3.5 的 MTP context 会强制走 plain KV,不要按 hybrid 去猜。

4. 图复用:decode 快,快在少建图

逐步生成时,每步通常只有输入数据变了,拓扑没变。llm_graph_params.allow_reuse() 比较 ubatch 形状、序列集合、sampler 指针;llm_graph_result.can_reuse() 还要所有 input 节点同意。匹配就跳过建图和 buffer 分配,只 set_inputsgraph_computen_reused 可以从 llama_perf_context() 看到。

复用会失效的常见原因:LLAMA_GRAPH_REUSE_DISABLE=1、从长 prefill 切到单 token、并行序列数变化、KV defrag / shift、LoRA 或 Control Vector 变更。Pipeline parallel 复用前必须 ggml_backend_sched_synchronize(),否则 GPU 还在读上一份 input。这是正常路径,不是故障。

把这一章压成一句话:llama_decode 先让 batch 合法,再让 Memory 有位子,然后尽量复用旧图去计算。下一章《llama.cpp:llama-server 与生产部署》会把同一套 decode 接到 HTTP:队列、slot 与连续批处理,就是在多个会话之上调度这些 ubatch。

llama.cpp:llama-server 与生产部署

llama.cpp:llama-server 与生产部署

「本系列第 8/17 章」。前两章已经说明:llama_model 只读可共享,llama_context 不能跨线程乱调,llama_decode 按 ubatch 推进。llama-server 并不另造一套推理引擎,它只是把这些约束做成可并发的 HTTP 服务。源码在 tools/server/,HTTP 用 cpp-httplib,JSON 用 nlohmann/json。

1. 请求怎么走进 decode

服务端主路径可以看成四段:

1
server-http.cpp  →  server-queue.cpp  →  server-context.cpp (slots)  →  server-chat.cpp

server-http 负责路由与协议。请求进入 queue 后排队、合并、调度。真正算 token 的是 context slots:每个 slot 对应一份推理会话,内部仍是上一章的 memory 与 llama_decodeserver-chat.cpp 把 OpenAI 风格的 messages 收成模型能吃的 token 序列(Jinja chat template 在 common/chat.cpp),再把采样结果写回响应。Function calling 在 server-tools.cpp,多模型路由在 server-models.cpp

--parallel 控制 slot 数量。slot 不是「开几个线程就安全」,而是「同时能挂起几路会话」。上一章写过 context 非线程安全:服务端用 slot 把并发请求隔开,再在调度器里决定哪些序列可以放进同一次 batch。slot 太少,请求在 queue 里等;slot 太多,每路都要占一份 KV,内存会先于 CPU 撑满。

2. 连续批处理与 SSE

生产环境很少「一个人 prefill 完再独占整段 decode」。连续批处理(--cont-batching,默认打开)允许不同请求处于不同阶段:有的还在灌 prompt,有的已经在逐 token 生成,调度器把它们合成一次 decode。新请求可中途加入,完成的序列立刻释放 slot。吞吐来自「同一张图里塞进更多活着的序列」,而不是无限加进程。slot 还可以 seq_cp fork、保存 / 恢复。

流式输出走 SSE。客户端把 stream: true 打开后,每个新 token 以 data: {"choices":[{"delta":{"content":"..."}}]} 推送,最后 data: [DONE]。这与 decode 循环一一对应:每成功一步 llama_decode 并采样,就多写一帧。不要把 SSE 理解成另一个推理后端。

投机解码用 -md draft.gguf --draft-max 16:draft 模型先吐若干 token,target 模型一次性验证。多模型目录用 --models-dir。这些都建立在同一套 slot / ubatch 之上。

3. 先认这些端点

端点 用途
GET /health 健康检查,部署探活
GET /props 当前服务属性
GET /v1/models 模型列表,LlamaIndex 先对这个
POST /v1/chat/completions Chat 补全,可 SSE
POST /v1/completions 文本补全
POST /v1/embeddings 向量,embedding 进程才开
POST /tokenize /detokenize /apply-template 排词表与模板
GET /metrics 需要 --metrics

探活用 /health,确认加载用 /v1/models,对话走 /v1/chat/completions。模板或词表异常时,先打 /tokenize 看 token 是否符合预期。还有 Anthropic 风格的 /v1/messages、以及 /infill/slots,不是最小闭环必需。

关键旋钮:-c 上下文、-b / -ub batch、-ngl 卸层、-fa Flash Attention、--host / --port。环境变量 LLAMA_ARG_THREADSLLAMA_ARG_CTX_SIZELLAMA_ARG_N_GPU_LAYERSLLAMA_ARG_HOST / PORT 可以少写命令行。默认 --host127.0.0.1,局域网访问必须改成 0.0.0.0

4. 冒烟部署

编译与 GGUF 仍沿用第 5 章:静态链接、GGML_NATIVE=OFF,需要界面时备好 tools/ui/dist。模型继续用 Qwen2.5-0.5B-Instruct-GGUFq4_k_m,先证明链路,再换大模型。

1
2
3
4
./build/bin/llama-server \
-m qwen2.5-0.5b-instruct-q4_k_m.gguf \
--host 0.0.0.0 --port 8080 \
-c 8192 -t 4 --parallel 4 --verbose

必须等到 HTTP server is listening。先打 /health,再发一条最短的 /v1/chat/completions。slot 不够会排队,KV 满会从 llama_decode 返回 1 一路冒到请求失败——这时应回到上一章检查 n_ctx 与 memory,而不是先改 HTTP 框架。集成测试在 tools/server/tests/,用 pytest 打 chat / tool / vision / slot。

llama-server 是调度壳,不是新的模型格式。权重仍是 GGUF,计算仍是 libggml,会话仍是一份份 context。推理这一段到此闭环。下一章《LLaMA Factory:环境、双架构与调用链》转回训练侧:在 Hugging Face 权重上做微调,再经第 5 章那条转换链回到 GGUF。

LLaMA Factory:环境、双架构与调用链

LLaMA Factory:环境、双架构与调用链

本系列第 8/17 章。分析基线是 LLaMA Factory 0.9.6.dev0。从本章起连续四章阅读同一仓库,顺序不要跳:先分清环境与架构,再进入配置与训练,然后是推理与界面,最后收束到对齐与分布式。本章只回答三个问题:安装时哪些版本不能错、v0 与 v1 究竟是什么关系、一条训练命令如何走到 Trainer。三个问题都清楚了,再打开下一章的 YAML 才不会把字段读错位置。

把这三件事放在最前面,是因为后续几乎所有误读都从这里开始。依赖装错,后面的 YAML 再正确也会在 import 或 parser 处失败;把 v1 当成 v0 的加速开关,会整栈换掉 launcher;找错入口,多卡训练甚至不会启动 torchrun。先建立地图,再填细节。后面三章默认读者已经接受这张地图。

1. 环境:硬约束与验收

仓库用 Hatchling 构建,包名 llamafactory,源码采用 src/ 布局。pyproject.tomlrequires-python>=3.11。基础安装会同时带上训练、Web UI 与 API 依赖,仓库没有把它们拆成可选 extras。因此“只想装一个最小训练包”在当前版本里并不存在;可选能力另外放在 requirements/ 下,例如 bitsandbytes、DeepSpeed、vLLM、SGLang,由运行时检查决定缺了哪一项。

训练核心的版本边界必须同时看下限、上限和排除项:

约束
torch >=2.4
transformers >=4.55<=5.6,且排除 4.57.0
peft 0.18.x

transformers!=4.57.0 是显式排除,不是文档笔误。无约束执行 pip install -U transformers peft,很容易把排除版本或越界的 PEFT 装回来。机器上的 CUDA、驱动、显存属于硬件策略,不是仓库硬约束;官方 Docker 镜像用过 Ubuntu 22.04、CUDA 12.4、Python 3.11、PyTorch 2.6.0,那只是镜像构建环境,不等于唯一支持组合。Python 用系统包、虚拟环境还是 Conda,同样属于机器策略,仓库只要求正式版解释器满足下限。同一前缀里不要混用多种解析器反复安装。

两个控制台入口完全等价,都注册到 llamafactory.cli:main

1
2
3
llamafactory-cli ─┐
├─> llamafactory.cli:main
lmf ──────────────┘

安装后先做验收,不要立刻下载大模型:

1
2
3
llamafactory-cli version
llamafactory-cli env
llamafactory-cli train -h

version 应打印 0.9.6.dev0env 打印 Python、PyTorch、Transformers、Datasets、Accelerate、PEFT,以及可选的 TRL、DeepSpeed、bitsandbytes、vLLM。默认 dataset_dir 是相对路径 data:在仓库根目录运行,或把路径改成绝对路径。CPU 可以验证导入与参数解析,但不能把“大模型训练可用”当成安装通过的标准。可选依赖按需安装即可:用到 4/8 bit QLoRA 再装 bitsandbytes,用到 ZeRO 再装 DeepSpeed,用到对应推理后端再装 vLLM 或 SGLang。解析器会在功能被启用时检查这些包,空装一堆用不到的 extras 并不能让验收更稳。

2. 双架构:整栈切换,不是局部优化

cli.py 在导入业务模块之前判断 USE_V1。因此 v1 不是 v0 Trainer 上的一个布尔开关,而是命令分发、配置系统、数据引擎、模型加载与训练循环的整体替换。两套体系可以维护各自的依赖层次,代价是配置文件和命令不能互换。

1
2
3
4
5
6
7
llamafactory-cli ...


cli.py::main

USE_V1 未启用 ──► src/llamafactory/launcher.py (默认 v0,稳定主路径)
USE_V1=1 ──► src/llamafactory/v1/launcher.py (实验路径)

默认 v0 使用扁平 YAML/JSON,经 OmegaConf 合并后再交给 Hugging Face 的 HfArgumentParser。v1 使用独立的 v1/config/,大量字段改成嵌套插件块,例如分布式、优化器、PEFT 各有 name。v1 当前没有 apiwebuiexport 路由;envversion 在 v1 launcher 中仍抛 NotImplementedError。不能把一份 v0 YAML 加上 USE_V1=1 原地迁移,默认遇到遗留字段会直接报错。实验命令应从 examples/v1/ 选取,例如 USE_V1=1 llamafactory-cli sft examples/v1/train_lora/train_lora_sft.yaml

阅读源码时先走完一条 v0 训练链,再对照 v1。同名概念——stage、LoRA、FSDP、DPO——在两套实现里不保证字段、默认值或 YAML 结构一致。把 v1 理解成“另一份产品”,比理解成“v0 的新版本开关”更不容易走错目录。

为什么必须在 cli.py 里、而不是在 Trainer 里切换?因为两套 launcher 随后会导入完全不同的配置类与训练器。若先导入 v0 的 hparams 再试图启用 v1,进程里已经留下一套 dataclass 与依赖假设。先判断环境变量、再导入对应 launcher,才能让两套体系各自保持干净的依赖树。这不是风格偏好,而是当前源码的分叉位置。

3. 默认 v0 的训练调用链

多卡必须使用 llamafactory-cli train。launcher 在命令为 train,并且满足“FORCE_TORCHRUN=1,或设备数大于 1 且未启用 Ray/KTransformers”时,会用 subprocess 拉起 torchrun ... src/llamafactory/launcher.py。子进程执行文件底部的 __main__,绝对导入并调用 run_exp(),不再进入 launch(),因此不会无限重启。src/train.py 只是薄入口,不含自动 torchrun。调试器若看到父子两次进程,这是多卡启动的预期现象。

单进程路径可以写成:

1
2
3
4
5
6
7
cli.main
→ launcher.launch
→ train.tuner.run_exp
→ parser 解析五个 dataclass
→ 按 stage 进入 workflow
→ get_dataset + load_model
→ Trainer

五个训练 dataclass 的顺序固定:ModelArgumentsDataArgumentsTrainingArgumentsFinetuningArgumentsGeneratingArguments。stage 的合法值只有 pt|sft|rm|ppo|dpo|kto。ORPO、SimPO、IPO 不是额外 stage,它们是 stage: dpopref_loss 的取值。把营销名写成独立 stage,会在参数校验处失败。

v0 的分层大致是:入口层(cli、webui、api)调用用例层(train、chat、eval),用例层再组合 data、model、hparams,底层工具在 extras。Web UI 最终仍生成参数并启动 CLI 子进程,不是另一套训练实现。API 复用 chat 门面。扩展通常沿着一条纵向链路改动:新模板改 template.py,新数据格式改 converter,新 stage 要同时改 workflow、tuner 路由和数据 processor。

也可以用目录来记责任。src/llamafactory/launcher.py 负责命令与多卡重启;hparams/parser.py 负责输入契约;train/tuner.py 负责 stage 与特殊后端路由;data/loader.pymodel/loader.py 分别是数据和权重的总入口;chat/api/webui/ 只是同一套核心能力的三种呈现。eval/ 仍留在仓库里,但 CLI 的 eval 命令已经直接不可用,不要从目录名反推命令表。实验代码全部在 src/llamafactory/v1/,测试则分成 tests/tests_v1/

环境出问题时应按“解释器 → 依赖 → 硬件 → 参数”的顺序缩小范围。先看 python --version 是否满足 3.11,再 llamafactory-cli env 核对 Transformers 与 PEFT 是否落在边界内,然后检查 torch.cuda.is_available(),最后才用 train -h 或小样本训练验证解析。报告问题时带上 env 输出、实际命令、YAML 和第一条完整异常,比只说“装好了但跑不起来”有效得多。

把调用链当成一张交通图,而不是一串文件名。用户只接触 llamafactory-clicli.py 决定走哪座城市(v0 或 v1);v0 的 launcher.py 决定是单车还是车队(单进程或 torchrun);run_exp 才是真正开始干活的车站。车站里先检票(五个 dataclass),再按车票上的 stage 把旅客送到对应车间。车间里的两道工序几乎总是成对出现:先把数据变成张量,再把模型放到可训练状态,最后交给 Trainer 循环。后面三章分别展开检票规则、车间工序,以及车间之外的售票窗口(Board 与 API)。先有这张图,读源码时才知道自己站在哪一层。

4. 本章应先记住的边界

环境、架构与入口一旦记错,后面三章的配置都会“看起来正确、运行不起来”。请先核对这四条:

  • 依赖要同时满足下限、上限和排除项;peft 锁在 0.18.xtransformers 不能是 4.57.0
  • llamafactory-clilmf 是同一入口,真正的分叉发生在 USE_V1
  • USE_V1=1 换掉整个 launcher,v1 没有 API、Board 与 export,也不能原地使用 v0 YAML。
  • 多卡训练入口是 llamafactory-cli train,不是 src/train.py;调用链的枢纽是 run_exp 与五个 dataclass。记住入口,比记住某一个 YAML 字段更能避免整章后面的误操作。

下一章把这份调用链展开为可写的 YAML、可对齐的数据 schema,以及六个 stage 共用的训练骨架。请继续阅读:LLaMA Factory:配置、数据模块与训练流水线

LLaMA Factory:配置、数据模块与训练流水线

LLaMA Factory:配置、数据模块与训练流水线

本系列第 9/17 章。第 8 章已经把命令送到 run_exp。本章解释这份入口之后的三份契约:参数如何变成五个 dataclass、原始数据如何变成 token、stage workflow 如何装配 Trainer。基线仍是 0.9.6.dev0 的默认 v0。读完应能独立写一份可解析的 SFT 配置,并知道 DPO 为什么看起来“像 RM”。

1. 配置:一种文件,两套语法

推荐路径是 YAML 加 OmegaConf 覆盖。覆盖项写在文件路径后面,没有前导 --

1
2
llamafactory-cli train examples/train_lora/qwen3_lora_sft.yaml \
max_samples=16

这是仓库里真实存在的最短冒烟命令。它验证的是解析、数据对齐、模型加载与 Trainer 起步,不是完整收敛。需要隔离输出目录时,可以再覆盖 output_diroverwrite_output_dir,但那不是验收所必需的最小集合。先跑通再加长,比一开始就写满超参更符合这条流水线的调试方式。

纯 CLI 则走 HfArgumentParser,必须写成 --learning_rate 这种形式。两种语法不要写在同一条命令里:YAML 后的 learning_rate=1e-5 与纯 CLI 的 --learning_rate 1e-5 不是同一套解析器。若在 Python 里直接调用 run_exp(args=dict)read_args() 会原样返回传入对象,不再读 sys.argv

read_args()sys.argv[1] 的后缀:.yaml/.yml 用 OmegaConf 加载;.jsonjson.load 再转 OmegaConf;随后把后面的 key=value 合并进去,后写的覆盖先写的。未知键默认报错,只有 ALLOW_EXTRA_ARGS=1 才允许多余字段。生产配置不应依赖这个逃生口,它会把拼写错误藏起来。

训练 parser 固定按这个顺序构造五个对象:ModelArgumentsDataArgumentsTrainingArgumentsFinetuningArgumentsGeneratingArguments。解析之后还有跨对象校验与派生,例如计算精度、device_map、截断长度。训练路径拒绝非 Hugging Face 的 infer_backendinfer_backend 的合法值是 huggingface|vllm|sglang,但后两个只属于推理配置;训练 YAML 里写 vllm 会被 parser 拒绝。

QLoRA 必须写对枚举。quantization_method 的 bitsandbytes 取值是 bnb,不是 bitsandbytes;位数写在 quantization_bit,通常是 48。当前准确示例是 examples/train_qlora/qwen3_lora_sft_bnb_npu.yaml。仓库里不存在名为 examples/train_qlora/qwen3_lora_sft.yaml 的文件,不要按旧笔记去找。

2. 数据:三次 schema,而不是一次读入

训练 workflow 调用 get_dataset()。数据不是“读完 JSON 直接喂 Trainer”,而是经过三次形态变化。第一次是用户原始列,由文件自己决定。第二次是 converter 对齐后的统一列:_prompt_response_system_tools_images_videos_audios。第三次是 stage processor 产出的 token 列,再交给 collator 做 padding 与 mask。

内置 converter 只有三种:alpacasharegptopenai。它们注册在 data/converter.py,可以通过注册函数扩展,但不能指望任意 JSON 结构被自动识别。data/dataset_info.json 描述来源、formatting、列映射以及 ranking 等属性。来源可以是 Hub、脚本、本地文件或有限的对象存储路径;Hub 选择还受 USE_MODELSCOPE_HUB 等环境变量影响。默认目录仍是相对路径 data,这是第 8 章验收时已经强调过的。

三种 converter 对“对话”的理解并不相同,迁移数据时不能只改文件名。Alpaca 把 history 展开成多轮,再把当前 promptquery 拼成最后一条用户话;普通样本只产生一条助手回答,ranking 样本则写出 chosen 与 rejected 两条,KTO 则按布尔标签把真实回答放进两个位置之一。ShareGPT 要求角色严格交替,奇数位是 user 或 observation,偶数位是 assistant 或 function;异常样本不会立刻从数据集删除,而是变成空 prompt,随后由 processor 丢弃。OpenAI 格式会处理 tool_calls,并把连续的 tool 结果合并为一条 observation;在没有 tools 时,这个版本还会注入或追加一段关闭详细思考的 system 文本。这是 converter 特有行为,不是模板层的通用默认值。

五个 dataclass 也可以按职责来记,避免把所有字段都塞进一个“训练配置”印象里。ModelArguments 管权重路径、量化、推理后端和导出;DataArguments 管数据集名、目录、截断长度与模板;TrainingArguments 继承 Hugging Face 的训练循环参数;FinetuningArguments 管 stage、微调类型和对齐损失;GeneratingArguments 管生成式评估时的解码。字段写错位置,表面上看 YAML 很完整,parser 仍会按类边界拒绝或忽略你的意图。

processor 由传入的数据 stage 决定,不由算法营销名决定:

传入数据 stage 实际处理
pt 预训练拼接
sft 监督微调;开启 packing 时换 packed 实现
rm 成对偏好
kto 反馈样本,并构造 KL 样本与 kto_tags
ppo 以及生成式评估 无监督 prompt

数据层的 stage 类型里 没有 dpo。DPO workflow 调用的是 get_dataset(..., stage="rm"),与奖励模型共用 pairwise processor,并要求数据集声明 ranking=true。这只表示复用“同一 prompt 下两条回答”的数据形态,并不表示 DPO 在训练奖励模型。把 DPO 数据写成普通 SFT 对话、或不设 ranking,会在预处理阶段丢掉合法样本。

DatasetModule 只有 train_dataseteval_dataset 两个键,没有 predict_dataset。预测复用验证集。显式 eval_datasetval_size > 0 互斥。加载和 tokenize 包在 main_process_first 里,避免多进程同时写缓存。

3. 训练:先路由,再走公共骨架

run_exp() 先规范化参数,再决定是否进入 Ray。普通路径进入 _training_function():解析五元组、按固定顺序追加回调,然后路由到某个 workflow。回调始终包含日志;其后按需加入 PiSSA 转换、SwanLab、早停、性能分析,最后是上报配置的 Reporter。调用方传入的回调会留在列表前部。

路由优先级不是“只看 stage”。若 pt/sft 且开启 use_hyper_parallel,进入 HyperParallel;否则若 pt/sft/dpo 且开启 MCA,进入 MCA;都不命中,才按 pt|sft|rm|ppo|dpo|kto 进入默认目录。开关与 stage 不匹配时落回默认路径,而不是报“后端不支持”。因此不能只看 YAML 里出现过某个字段,就断定最终走了哪条后端。

六个默认 workflow 共享同一骨架:先 load_tokenizer,再 get_template_and_fix_tokenizer,然后 get_datasetload_model,接着构造 stage 专用 collator 与 Trainer,最后按 do_train / do_eval / do_predict 执行并保存。模板修复必须发生在模型加载之前,因为它可能补充 EOS、PAD 与 stop words;模型再按 resize_vocab 调整 embedding。SFT 是最完整的一条,生成式预测会切到左 padding,并使用 predict_with_generate。RM 会加 value head,保存时还要把 v_head 拆到独立文件。

请把 stage 与损失再分开记一次。合法训练 stage 只有六个,多写一个名字不会被自动纠正。pref_loss 的精确取值是 sigmoid|hinge|ipo|kto_pair|orpo|simpo,它们全部挂在 stage: dpo 下面。ORPO 与 SimPO 尤其容易被写成独立阶段,这是文档宣传与源码枚举不一致造成的认知陷阱。

多卡仍然必须走 llamafactory-cli train,以便 launcher 自动 torchrunsrc/train.py 不会替你拉起多进程。DeepSpeed 配置在解析阶段还可能要求 FORCE_TORCHRUN=1。这些进程问题属于第 11 章,但写 SFT YAML 时就应避免选错入口。

也可以把一次成功的 SFT 想象成四次“对上号”。第一次,文件路径与 key=value 对上 read_args 的合并规则。第二次,字段名对上五个 dataclass,而不是对上一份自己发明的扁平字典。第三次,原始列对上 converter,再对上 _prompt/_response 这组统一列。第四次,数据 stage 对上 processor,训练 stage 对上 workflow。任何一次对不上,失败点都不同:第一次是命令行语法,第二次是未知参数,第三次是空样本或字段缺失,第四次是训到了错误的损失或错误的模型头。冒烟命令的价值,就是用十六个样本把这四次对齐一起跑通。

4. 写配置时最容易错的四处

把配置、数据与训练看成三份契约,比记住几十个字段更有用。交作业前先核对:

  • YAML 覆盖与纯 CLI 是两套语法;未知键默认失败,不要靠 ALLOW_EXTRA_ARGS 蒙混。
  • QLoRA 写 quantization_method: bnb,不要写 bitsandbytes
  • DPO 的训练 stage 是 dpo,数据层看到的却是 rm;ORPO/SimPO 只是 pref_loss
  • 训练配置里的 infer_backend 只能是 Hugging Face;vLLM 与 SGLang 留给下一章的推理入口。

下一章离开 Trainer,进入推理引擎、LLaMA Board 与 OpenAI 风格 API。请继续阅读:LLaMA Factory:推理、LLaMA Board 与 OpenAI API

LLaMA Factory:推理、LLaMA Board 与 OpenAI API

LLaMA Factory:推理、LLaMA Board 与 OpenAI API

本系列第 10/17 章。第 9 章把样本送进 Trainer 并写出 checkpoint。本章说明训练完成后的三条出口:程序与终端推理、LLaMA Board,以及 OpenAI 风格 HTTP API。基线是 0.9.6.dev0 的 v0。v1 没有 apiwebuiexport,不能把本章命令加上 USE_V1=1 指望原样工作。

三条出口共用同一扇门:ChatModel。Board 的聊天页在 Web 进程里构造它,API 进程在启动时构造它,终端 chat 命令也构造它。真正分叉的是 infer_backend,以及 Board 训练页会不会再去拉起一条 CLI 子进程。把“界面”和“训练实现”当成两套系统,是本章最需要先拆掉的误解。

可以先用一句教科书写法记住分工:训练改变权重,推理消费权重,Board 把两者的命令拼出来,API 把推理包装成 HTTP。权重如何更新,仍然回到第 9 章的五个 dataclass 与 stage workflow;权重如何被问话,则全部经过本章的引擎门面。界面上的按钮如果看起来“也能训练、也能聊天”,只是因为它同时调用了这两条已经存在的路径。

1. 推理:一个门面,三种后端

ChatModel 先调用 get_infer_args(),再按 infer_backend 选择引擎。合法值只有三个:

取值 引擎 进程模型 打分
huggingface HuggingfaceEngine 本进程 Transformers 支持 get_scores()
vllm VllmEngine 本进程 AsyncLLMEngine 不支持
sglang SGLangEngine 子进程 HTTP 服务 不支持

训练 parser 拒绝非 HF 后端。因此同一字段出现在训练 YAML 与推理 YAML 中,含义并不对称:训练里写 vllm 会失败,推理里它才是合法加速路径。HF 引擎在 stage == sft 时才能生成;非 SFT 会加载 value head,此时 chat 被拒绝,只保留打分。vLLM 与 SGLang 没有这条打分路径。

门面对上提供同步与异步两套方法:chatstream_chatget_scores,以及对应的 a* 形式。异步方法直接 await 引擎,FastAPI 走这条路。同步方法把协程提交到后台 event loop 线程,再阻塞等待。对象没有显式 close(),后台线程随进程退出;反复新建 ChatModel 会累积 daemon 线程,服务进程应当复用单例。

三个后端的实现差异大于接口差异。HF 非流式调用 model.generate(),流式再用 TextIteratorStreamer 加生成线程。vLLM 只让 LLaMA Factory 负责模板编码,执行交给 AsyncLLMEngine;存在 adapter 时通常只取第一个做成 LoRARequest。SGLang 则拉起 sglang.launch_server 子进程,请求发到 /generate,同样只加载第一个 LoRA。HF 当前会忽略请求级 stop 字符串并打警告;vLLM 与 SGLang 支持 stop。SGLang 不支持 n > 1。不要仅凭方法签名判断多模态是否真正进入引擎:SGLang 路径发送的是 input_ids

终端入口使用推理配置,而不是训练配置:

1
llamafactory-cli chat examples/inference/qwen3_lora_sft.yaml

2. Board:生成 CLI,而不是另一套 Trainer

1
llamafactory-cli webui

webchat 只装配聊天界面,适合已经写好推理 YAML、只想对话的场合。完整 Board 包含 Train、Evaluate & Predict、Chat、Export。默认监听地址是 0.0.0.0,可用 GRADIO_SERVER_NAME 修改;端口走 Gradio 自身默认值或 GRADIO_SERVER_PORTGRADIO_SHARE=1 会打开 Gradio 的 share。这些都是界面进程的部署参数,不改变训练实现。

Board 的训练能力来自 Runner:把页面字段收成参数字典,再启动 llamafactory-cli train 子进程并监控日志。它是 CLI 的参数生成器与过程监视器,不是第二套 Trainer。Chat 在 Web 进程内构造 ChatModel;Export 在 Web 进程内直接调用 export_model(),要求提供 export_dir。刷新页面后的“恢复”,只是重新挂接当前 Python 进程里还活着的子进程。关掉 Web 进程再打开,并不会从磁盘自动复活一次训练;断点续训由输出目录和训练参数完成。

Evaluate & Predict 最容易与旧评测命令混淆。它仍然走训练入口,强制 stage=sftpredict_with_generate=true;勾选预测则 do_predict,否则 do_eval,并始终设置 eval_dataset。因此它是数据集上的生成式评估,沿用第 9 章那条 SFT workflow,不是独立评测框架。与此同时,llamafactory-cli eval 在 launcher 里直接抛出 NotImplementedError,源码注明将来弃用。这条命令不会“警告一下再继续跑”。需要指标时,用 Board 的 Evaluate,或在训练 YAML 里写 do_eval / do_predict

Board 顶部的模型、模板、量化选项由四个 Tab 共享。量化对 LoRA/OFT 开放;Chat 页的后端同样是 huggingface|vllm|sglang。演示模式可以显示训练页,但 Runner 会拒绝真正启动任务。把 Board 理解成“给 CLI 填表”,比理解成“图形化训练框架”更接近源码。

界面内部按三层协作。interface.py 只负责装配 Tab 与事件;Engine 持有组件索引、Runner 和进程内聊天模型;Manager 把控件登记成稳定 ID,例如 train.datasetinfer.chat_box。新增一个输入框时,必须同时改组件注册、参数解析、恢复逻辑和文案,否则页面上看得到、启动训练时却读不到。Train 页的 extra_args 必须是 JSON 对象,并且最后覆盖 Board 生成的同名字段。它适合补齐界面没有画出的项,不适合把整份训练契约都藏进一个文本框。

HF 后端的 MAX_CONCURRENT 只是并发信号量,不会自动做 continuous batching。把它调大可能提高峰值显存,却不会变成 vLLM 的调度器。需要吞吐时,应换 infer_backend,而不是在 Board 里把并发数字调到很大还继续用 Transformers 生成。这与第 9 章“训练拒绝非 HF 后端”并不矛盾:训练要的是可反传的模块,推理要的是生成调度。

3. API:三条 OpenAI 风格路由

1
llamafactory-cli api examples/inference/qwen3_lora_sft.yaml

默认监听 0.0.0.0:8000,可用 API_HOSTAPI_PORTAPI_KEY 覆盖。run_api() 自行创建 ChatModel,再交给 Uvicorn。应用启用宽松 CORS;公网部署应在反向代理收紧,不要把默认值当成安全基线。

当前只注册三条路由:

方法 路径 何时可用
GET /v1/models 总是
POST /v1/chat/completions 引擎可生成;否则 405
POST /v1/score/evaluation 引擎不可生成,走奖励打分;否则 405

API_KEY 非空时,三个端点都做 HTTP Bearer 校验。/v1/models 返回的模型 ID 来自 API_MODEL_NAME,默认 gpt-3.5-turbo。这是协议里的名字,不是磁盘上的权重路径。聊天与打分互斥:SFT 生成引擎只开 completions,value-head 引擎只开 score。客户端若对生成模型调用 score,或对奖励模型调用 chat,得到的是 405,而不是“自动降级”。这是协议层用状态码表达引擎能力,不要在网关里把它改写成 200 空回复。

部署时还要分清“谁在监听、谁在算、谁在训”。Board 进程负责画界面;一旦点击训练,真正占 GPU 做反传的是它拉起的 CLI 子进程。Chat 与 API 则在当前进程里加载引擎,HF 与 vLLM 占本进程显存,SGLang 再额外占一个子服务。因此同一台机器上同时开 Board 训练、Board 聊天和 API,会叠出多份模型。正确做法是:训练时关掉聊天引擎,提供服务时只留一个 ChatModel 单例。API_KEY、监听地址和 CORS 属于访问控制,不是功能开关;功能开关仍然是 stage 与 infer_backend。把安全参数和算法参数混在一起改,排错时很难判断是鉴权拒绝还是引擎能力不足。

4. 怎样选择出口

交互试错用 chat 或 Board 的 Chat,二者都是 ChatModel。要把已有 OpenAI SDK 接过来,用 api,只依赖上述三条路径。需要改超参并启动训练,用 Board 的 Train,但要清楚最终仍是 llamafactory-cli train。需要生成式指标,用 Board Evaluate 或训练配置里的 do_eval/do_predict,不要调用已经拆除的 eval 命令。导出合并走 export 或 Board 的 Export,不要在量化模型上再合并 adapter。选出口时先问“要改权重还是只用权重”,答案会直接指向 Train 或 Chat/API。

下一章把对齐算法、多卡进程模型和 v1 边界放到同一张地图上。请继续阅读:LLaMA Factory:对齐训练、分布式与进阶能力

LLaMA Factory:对齐训练、分布式与进阶能力

LLaMA Factory:对齐训练、分布式与进阶能力

本系列第 11/17 章,也是 LLaMA Factory 四章中的收束章。基线仍是 0.9.6.dev0。前三章已经能在单机上跑通 SFT、看懂推理出口。本章把三件容易缠在一起的事情拆开:对齐算法属于哪个 stage、多卡到底打开了哪一层、v1 与那些环境变量开关各自住在哪里。读完应能解释为什么 ORPO 不能写成独立阶段,以及为什么只写 ray_num_workers 并不会启动 Ray。

1. 对齐:四个入口,六种损失

训练 stage 的合法值仍然只有 pt|sft|rm|ppo|dpo|kto。与对齐直接相关的是后四个。它们共用第 9 章那条骨架——tokenizer、模板、数据集、模型、collator、Trainer——但数据形态和模型角色不同。

奖励模型 rm 使用成对的 chosen/rejected,加载带 value head 的因果语言模型,损失是 Bradley–Terry 风格的配对比较。保存时必须把 v_head 拆到独立文件,直接把包装器的 state dict 当普通模型导出会得到错误键名。PPO 同时需要三类模型:待优化的策略、用于 KL 约束的参考模型,以及给出标量奖励的奖励模型。奖励来源可以是独立完整模型、挂到同一底座上的 LoRA,或 HTTP API。PPO 必须提供 reward_model;它走自定义的 ppo_train() 循环,先生成再打分再更新,而不是普通 Trainer.train()

DPO 在训练参数里写 stage: dpo,但数据层调用的是 get_dataset(..., stage="rm")。这是复用 pairwise processor,不是“DPO 其实在训 RM”。KTO 不要求同一 prompt 下成对出现,只要独立的好/坏标签;processor 会额外构造用于估计 KL 的错位样本,并带上 kto_tags

ORPO 与 SimPO 不是独立 stage。它们和 sigmoidhingeipokto_pair 一起,构成 pref_loss 的六种取值,全部挂在 stage: dpo 下面。sigmoid/hinge/ipo/kto_pair 进入 TRL 的 dpo_loss()orpo/simpoCustomDPOTrainer 本地实现,并且不创建参考模型。pref_loss=kto_pair 仍是成对 DPO 损失,不等于 stage: kto。需要省掉参考模型显存时,应在 DPO 下选择 ORPO 或 SimPO,而不是去找一个不存在的 stage: orpo

选型可以按数据记忆。有同 prompt 的 chosen/rejected,用 DPO;若还要给后续 PPO 当评分器,先训 RM。只有零散的好/坏反馈,用 KTO。已经有可靠奖励模型,并且需要在线采样优化,用 PPO。标签可能有噪声时,DPO 的 sigmoid 可以配合标签平滑;这一项不能套到其他 pref_loss 上。v1 的 DPO 目前只接受部分损失,不能假定这六种都能原样搬过去。

参考模型何时出现,也值得单独记一笔。标准 DPO 需要参考概率:显式给出 ref_model 就独立加载;LoRA 且未指定时可以返回空,计算参考对数概率时临时禁用 adapter;全参或冻结微调且未指定时,会再加载一份冻结底座。ORPO 与 SimPO 关掉参考模型,所以同卡显存往往更宽松,但它们仍然是 DPO workflow,数据仍按配对 RM 语义处理。KTO 默认也使用参考模型,LoRA 时同样用禁用 adapter 的办法得到参考对数概率。不要看到“没有 ref_model 字段”就断定没有参考分布,也不要看到“DPO 数据 stage 是 rm”就去改奖励头。

2. 分布式:四层模型,三种开关形态

“多卡”常常被写成一个开关,源码里却至少有四层。最外层是进程启动:launcher.pyllamafactory-cli train 下自动 torchrun。多设备且未启用 Ray/KTransformers 时会重启;FORCE_TORCHRUN=1 也会强制这条路径。src/train.py 没有自动 torchrun,多卡必须走 CLI。再往里是集群调度,即 Ray 的 worker 与 placement group。再往里是参数与状态分片:DDP、DeepSpeed ZeRO、FSDP/FSDP2,由 Hugging Face Trainer 与 Accelerate 配置驱动。最里层是替代训练后端:MCA 与 HyperParallel。KTransformers 是模型加载和算子后端,不是普通数据并行,也不是第四种推理 infer_backend

开关的写法不一致,这是配置错误的第一来源:

能力 开启方式 覆盖范围
Ray 环境变量 USE_RAY=1 调度 worker;内部仍跑原 Trainer
KTransformers 环境变量 USE_KT=1 排除自动 torchrun;训练侧限制为 LoRA
MCA 环境变量 USE_MCA=1 pt/sft/dpo;launcher 会强制 torchrun
HyperParallel YAML 字段 use_hyper_parallel pt/sft

只写 ray_num_workers 不会打开 Ray。USE_MCA=1 在导入参数类时就把 TrainingArguments 换成 Megatron 适配实现,不能在进程已经 import 之后再临时切换。USE_KT=1 会让“按可见设备数自动 DDP”失效,这是有意的。HyperParallel 与 MCA 在默认 stage 路由之前截获;stage 不匹配时静默落回默认 workflow,而不是报“后端不支持”。因此排错时要同时看环境变量、YAML 字段和最终进入的 workflow 目录。

DeepSpeed 通过 deepspeed: path/to/config.json 交给 Trainer;FSDP 通常先准备 Accelerate 配置再启动 CLI。Board 发现 DeepSpeed 参数时会设置 FORCE_TORCHRUN=1。Ray 示例存在于 examples/train_lora/qwen3_lora_sft_ray.yaml,但没有 USE_RAY=1 时这份 YAML 不会进入 Ray 分支。KT 面向 MoE 的异构加载,可与专用 FSDP2 配置组合,却不能写成 infer_backend: ktransformers

选型时按目标选层,而不是按名词堆叠。单机多卡的常规 SFT,让 CLI 自动 torchrun 再走 DDP 即可。优化器状态或参数显存不够,再加 DeepSpeed ZeRO 或 FSDP2。多机资源要交给集群调度时才开 Ray,并记住 Ray 只负责放置 worker,不会自动变成张量并行。需要 Megatron 那一套 TP/PP/EP 时才开 MCA;需要在 PT/SFT 上组合 FSDP/TP/CP 时才开 HyperParallel。巨型 MoE 的 CPU/GPU 异构加载才轮到 KT。把这些名字同时写进一份配置,并不会得到“全部加速”,只会得到互相抢入口的路由。

3. 进阶能力,以及 v1 为什么不能原地迁移

GaLore、APOLLO、BAdam、LoRA+、PiSSA、Liger、FP8 分散在三组参数里,分别注入自定义 optimizer、adapter 初始化或 kernel patch。它们多数互斥:LoRA 不能与 GaLore/APOLLO/BAdam 同时打开,后三者也不能彼此叠加。Web UI 只暴露其中一部分字段,完整契约仍是 dataclass 与 YAML。把这些算法理解成“再勾一个加速”,很容易在 __post_init__ 被拒绝。

v1 用 USE_V1=1 整栈切换,配置改成嵌套插件块,训练循环也不再复用 Hugging Face Trainer。它目前提供 sftdpormchatmerge,没有 v0 的 apiwebuiexport。第 10 章的 Board 与三条 HTTP 路由都属于 v0。v0 YAML 不能原地迁移:字段名、默认值和结构都不同,默认遇到遗留键会报错。要把实验路径跑起来,应使用 examples/v1/,而不是给旧文件加一个环境变量。

无论对齐还是分布式,都建议先用第 9 章那条冒烟命令证明主路径仍然健康:

1
llamafactory-cli train examples/train_lora/qwen3_lora_sft.yaml max_samples=16

确认解析、数据与 Trainer 可用后,再改 stage 或加一层分布式。一次只改一层:先算法,再进程启动,再 ZeRO/FSDP,最后才是 MCA、HyperParallel 或 KT。把文档里出现过的名字写成独立 stage、把环境变量写成 YAML 字段、或把 v1 当成 v0 的加速开关,都会在 parser 或 launcher 处失败。按调用链核对,比按功能清单记忆更稳。

把对齐与分布式放在同一章,是因为两者都会改“入口之后的路由”,却改的不是同一层。换 stagepref_loss,改变的是损失与数据形态;换 USE_RAYuse_hyper_parallel,改变的是进程与并行实现。先改损失再改并行,失败时至少知道应当看 Trainer 还是看 launcher。反过来同时改两层,日志里会出现“像算法错误的分布式问题”,或“像多卡问题的数据 stage 错误”。第 8 章的调用链在这里仍然够用:先问自己现在站在 launcher、parser、workflow 还是引擎门面。站对层,再查本章的表。

4. 四章应留下的对照表

  • 第 8 章:Python 与依赖边界、cli → launcher 双架构、多卡必须走 llamafactory-cli train
  • 第 9 章:五个 dataclass、数据的三层 schema、六个 stage 的公共骨架;QLoRA 写 bnb
  • 第 10 章:三种推理后端、Board 只是 CLI 子进程生成器、API 只有三条路由;eval 已拆除。
  • 本章:ORPO/SimPO 属于 pref_loss;Ray/KT/MCA 是环境变量,use_hyper_parallel 才是 YAML;v1 不能原地迁移。

至此,LLaMA Factory 主线四章读完。下一章进入本系列第 12/17 章。