先定位——它既不属于"能跑什么"(横向改装位),也不属于"跑起来之后怎么快"(纵向后半段),而是纵向主线的最前端:
请求进来 → 装模型 → step() 循环 → 吐字
↑
本篇在这里:循环开始之前
所以它是主线的"前传",我叫它 外传 · 前传:餐厅开张前的进货与摆盘。
前传:餐厅开张前的进货与摆盘
先说清楚:为什么加载值得单独一篇
前面讲的所有东西——FlashAttention、PagedAttention、连续批、调度器——有个共同前提:模型已经在显存里了。
但一个几百 GB 的模型,从磁盘到能跑,中间有很长的路。这条路慢起来是分钟级的:你上次那条日志里,光 load_model 就 218 秒。
所以这一篇回答一个问题:
那一堆上百 GB 的"零件",是怎么搬进厂房、组装成生产线、然后才开工的?
第一层:先用你的真实日志,看清加载到底慢在哪
这一步比文档讲的任何理论都有价值,因为它能直接检验文档的说法对不对。
回顾你贴的那段日志——里面有两个时间,含义完全不同:
Multi-thread loading shards: 100% | 74/74 [00:17<00:00, 4.30it/s] ← 17 秒
...
Loading model weights took 41.2679 GB
Model runner load_model total time: 218.68 seconds ← 218 秒
| 阶段 | 耗时 | 占比 | 在干什么 |
|---|---|---|---|
| 读文件(74 个 shard) | 17 秒 | 8% | 从磁盘把权重读进 CPU 内存 |
| 加载全流程 | 218 秒 | 100% | 上面那 17 秒 + 另外 200 秒 |
结论:读文件只占 8%。剩下 92%(约 200 秒)在做别的事。
那 200 秒在干什么:
| 动作 | 说明 |
|---|---|
| 搬运 | 权重从 CPU 内存搬到 NPU 显存(41.27 GB) |
| 量化处理 | --quantization ascend+ w8a8,要做量化变换,不是简单搬运 |
| TP/EP 切分重排 | 按并行规则把矩阵切开、分给各卡 |
| MTP draft 模型加载 | 日志里llm_base_proposer.py那些行 |
这个分解非常重要——它直接决定了文档推荐的优化手段对你有没有用。下面会反复回到这个数字。
第二层:三个技术逐个拆(餐厅版)
技术一:HuggingFace 集成 —— "按菜谱进货,按自家图纸摆盘"
0 基础理解
模型目录里存的是:一堆权重表格(safetensors 文件)+ 一个 config.json 说明书。
但光有一堆表格,不等于有个能跑的模型。就像你买回来一堆零件,还得知道"哪个零件装在哪"。
vLLM 的做法分三步:
① 读 config.json
→ 看到 architectures: "LlamaForCausalLM"
→ 查自己的"花名册",翻到对应那一页
② 拿出 vLLM 自己那套生产线图纸
(注意:不是 transformers 的原始图纸,是 vLLM 重画过的)
③ 把权重按图纸填进去
关键认知:vLLM 不是"跑" transformers 的代码
文档 1.2 这句话说得对:
vLLM 运行的是一套逻辑等价、但为自己引擎高度优化过的重实现。
为什么非要重画?因为 vLLM 的层要配合 PagedAttention——权重得填进"能接受 block_table 索引"的自定义层里。原始 transformers 的层没有这个接口。
打个比方:
transformers 的图纸:标准厨房,灶台固定位置
vLLM 的图纸: 同样的菜,但灶台位置按"传送带动线"重新排过
菜一样好吃,但出餐快得多
⚠️ 文档 1.1 的说法过时了
文档说:
"vLLM 底层仍然调用了 transformers 的
from_pretrained方法"
这个说法不准确。 vLLM 早就不用 from_pretrained 了——它用自己的 loader 直接读 safetensors 文件。
理由很简单:既然要把权重填进自定义的层结构,那走 transformers 的原始构建流程(先建标准模型、再拆、再搬)纯属绕远路。文档后面自己也打了补丁说"等效的底层代码(概念性)"。
以"概念性"为准,别以为真在调 from_pretrained。
对你日志的意义
你那 74 个 shard、17 秒读完 41 GB(约 2.4 GB/s),正是这三个参数的效果:
--safetensors-load-strategy 'prefetch' ← 预取
--model-loader-extra-config='{"enable_multithread_load": "true",
"num_threads": 128}' ← 128 线程并发读
IO 环节你基本已经优化到头了——这点下面还会说。
附带:load_format="dummy" 是什么
不是优化,是测试技巧:不加载真实权重,用随机值填,几秒起来。
对你的用途:调配置、验证启动流程时用它,能跳过那 218 秒。等配置确认无误了,再跑真实加载。这是你调试阶段最实用的一个开关。
技术二:Tensorizer —— "整店拍个快照,下次直接恢复"
0 基础理解
传统加载慢,慢在组装:
读文件 → 反序列化 → 创建 Python 对象 → 构建层 → 搬运 → 切分 → ...
Tensorizer 的想法极其直接:
既然组装这么慢,那我第一次组装好之后,把"组装完的最终状态"整个拍个快照存下来。下次直接把快照恢复回显存不就行了?
传统: 零件 → 一步步组装 → 生产线
Tensorizer:把组装好的生产线整个拍快照 → 下次直接快照恢复
文档那句"保存的不是如何构建结构的指令,而是权重在内存中的最终形态",说的就是这个。
但是——你的日志给出了一个明确的反证
文档宣称:能把分钟级压到 10~30 秒。
可你的日志显示:读文件只占 17 秒。就算把这 17 秒完全归零,你也只能省 8%。 剩下 200 秒的搬运、量化、切分,Tensorizer 一样都帮不上。
所以在你的场景里,Tensorizer 的收益远小于文档宣称。三条原因:
① safetensors 本身已经是 mmap 零拷贝了
现代 vLLM 默认的 safetensors loader 就用内存映射。Tensorizer 当年快,是相对老旧的 pickle / torch.load 而言的——现在两者差距已经大幅缩小。文档没说这个前提已经变了。
② 你是量化模型,量化发生在加载时
你的模型是 w8a8,--quantization ascend。量化变换是加载过程中现做的,不是预先算好存文件里的。而 Tensorizer 存的是"最终内存形态"——换个量化配置就得重新生成快照(文档 2.3 自己也承认这点)。
③ 昇腾上大概率根本用不了
Tensorizer 是 CoreWeave 为 NVIDIA GPU 做的,深度依赖 CUDA 和 GPU 架构(文档说"与 CUDA 版本、GPU 架构如 sm_80 强绑定")。你跑的是昇腾 NPU,这条路基本走不通。
结论
Tensorizer 对你这套配置基本不适用。 文档把它列为"生产部署首选",是基于 NVIDIA + 非量化模型的假设,跟你的场景不匹配。
技术三:分片加载 —— "零件直接分给 8 个车间,各装各的"
0 基础理解
这就是之前讲的 TP(张量并行) 在加载语境下的含义。
不分片: 每张卡都加载完整模型 → 装不下,且浪费
分片(TP=8):8 张卡各加载自己负责的 1/8
文档 3.1 的例子:8B 参数、TP=4 → 每卡约 2B 参数。
怎么切:切分规则预写在模型实现里
不是随便切,而是在模型代码里提前定义好每个矩阵怎么切(按列切还是按行切)。文档 4.3 提到的 _verify_and_get_linear_layer 就是干这个的。
这就是为什么 vLLM 要自己重画图纸——切分规则也得自己写进图纸里。
加载时的额外好处:省 IO
每个 worker 只读自己那份 → 磁盘读取总量不变,但每张卡的内存占用降到 1/N。
⚠️ 但你日志里有个数字对不上,值得你去核实
每个 worker 都报:
Loading model weights took 41.2679 GB
Actual usage: 41.27 GiB for weights
如果是 TP=8,每卡应该只占约 1/8 的权重,不该是完整的 41.27 GB。
三种可能:
- 实际是 TP=1、EP=8——注意力层每卡复制一份完整权重,只有专家被分散
- 这行日志打印的是"模型权重总量",不是"本卡占用量"
- TP/EP 的实际度数跟你以为的不一样
怎么核实:看你 run_p1.sh 里 --tensor-parallel-size 实际传的是几;再看日志里 Worker_TP2_EP2 这种编号的最大值——如果只到 TP1_EP8,那 TP 就是 1。
为什么这个数字重要:它决定了你整张显存账怎么算。如果每卡真是 41.27 GB 权重,那 60 GB 卡里剩给 KV Cache 的就那么点,跟你看到的 12.46 GiB 是对得上的;但如果 TP 真是 8,那整个账就不是这么算的。
第三层:文档问题清单
延续这个系列的一贯规律——讲思想靠谱,讲 API 基本过时:
| 位置 | 问题 |
|---|---|
| 1.1 | "底层仍调用from_pretrained"——已不准确,vLLM 用自己的 loader |
| 2.1 | Tensorizer 的收益是相对老式 pickle 加载而言,对现代 safetensors+mmap 边际收益大幅缩小,文档没说这个前提已变 |
| 2.2 | 建议enforce_eager=True配合 tensorizer——你确实开了--enforce-eager,但原因不是 tensorizer(你是因为 MTP/PD 分离下图模式有问题),别混淆 |
| 2.3 | "版本强绑定"说得对,但没明说只适用于 NVIDIA,昇腾用不了 |
| 3.3 | pipeline_parallel_size=2, tensor_parallel_size=4标注了"概念性示例"——V1 里 PP 支持有限,别当真 |
| 3.2 | placement_config手动指定设备,高级 API,可能变动,且不确定昇腾插件支持 |
| 4.2 | 通信库写的是NCCL(NVIDIA),你在昇腾上对应的是HCCL |
| 通篇 | 所有示例是V0 早期 API,你跑的是 V1 |
第四层:对你实际工作的三条建议
1. 别在"读文件"上再花力气了
17 秒 / 218 秒 = **8%**。你已经开了 128 线程 + 预取,IO 基本到头。继续加线程、换更快的盘,收益极其有限。
2. 218 秒启动时间,固定部署下可以不管
关键是这是一次性开销。服务起来跑几天,218 秒摊薄到近乎为零。
只有这三种场景才需要优化它:
| 场景 | 为什么 |
|---|---|
| K8s 按流量自动扩实例 | 每次扩容干等 3.6 分钟 |
| 需要快速故障恢复 | 实例挂了要秒级拉起 |
| 频繁改配置重启调试 | → 用load_format="dummy"更快 |
你是固定裸机、长期跑着的,这 218 秒根本不用管。
3. 如果真要优化,方向不是 tensorizer
在你场景下更实际的:
- 权重常驻高速盘/共享内存,避免每次冷读
- **
dummy模式验证配置**,最后一次真实加载 - ⭐ 确认量化能否离线预做——如果 w8a8 能在离线阶段算好直接存盘(而不是加载时现算),能省掉相当一部分的 200 秒。这可能是你唯一能吃到的、真正的加载加速,值得去查你们量化工具链支不支持"离线量化存档"。
知识树:补上这一篇
vLLM 知识体系
│
├─ 【前传·本篇】模型怎么装进来(step 循环之前)
│ ├─ HF 集成:读 config.json → 查注册表 → 按 vLLM 图纸填权重
│ ├─ 分片加载:TP=N,每卡只拿 1/N ← 日志 41.27GB 待核实
│ └─ Tensorizer:整机快照(NVIDIA 专属,你用不上)
│
├─ 【主线】装完之后怎么跑得快
│ ├─ FlashAttention / PagedAttention
│ ├─ Scheduler / Worker / Engine(连续批)
│ └─ 图模式
│
└─ 【外传】能跑什么
├─ 底盘变体:decoder-only / encoder-only
└─ 改装位:MoE / 多模态 / Embedding
一句总结
加载 = 搬零件 + 组装。你的日志显示搬零件只占 8%(17 秒),组装占 92%(200 秒)——所以文档主推的 Tensorizer(本质是加速搬零件)对你收益极小,且昇腾上大概率不可用。真正该做的不是加速加载,而是核实那 41.27 GB 是每卡量还是总量,以及量化能否离线预做。