qwen部署

Ikko Lv4

环境与部署

1
2
3
4
5
6
7
conda create -n vllm-qwen python=3.11 -y
conda activate vllm-qwen
python -m pip install -U pip setuptools wheel
pip install -U vllm
pip install -U "transformers>=4.56.0,<5" accelerate huggingface_hub "bitsandbytes>=0.49.2"
huggingface-cli download unsloth/Qwen2.5-14B-Instruct-bnb-4bit \
--local-dir ./models/qwen14b

这个模型必须通过两卡 + pipeline parallel + 降并发的方式部署,否则会因为显存不足而报错。

1
2
3
4
5
6
7
8
CUDA_VISIBLE_DEVICES=0,1 vllm serve /home/share/HDstorage/xyc/qwen/models/qwen14b \
--served-model-name qwen14b-pp2 \
--dtype bfloat16 \
--max-model-len 2048 \
--trust-remote-code \
--pipeline-parallel-size 2 \
--gpu-memory-utilization 0.6 \
--max-num-seqs 1

部署成功后,可以通过以下方式测试:

1
2
3
4
5
6
7
8
9
10
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen14b-pp2",
"messages": [
{"role": "user", "content": "你好,简单介绍一下你自己。"}
],
"max_tokens": 64,
"temperature": 0.7
}'

关闭服务:

1
pkill -f "vllm serve /home/share/HDstorage/xyc/qwen/models/qwen14b"

任务目标

这次工作分三段:

  1. 基于 vLLM 的 BitsAndBytes 量化支持,跑通小型 4-bit 模型推理。
  2. 分析 NF4 权重恢复在量化推理链路中的位置与开销。
  3. 基于 vLLM 的自定义量化扩展机制,设计实验性的 NF4 恢复/融合路径,并在 decode-like 小 batch 场景下评估其收益。

结论先说

这轮实验已经回答了几个关键问题:

  1. vLLM 跑 pre-quantized BitsAndBytes 4-bit 模型时,普通 dense 层并不是“先把 NF4 权重整层反量化到显存再计算”,而是尽量直接消费 packed weight 和 QuantState
  2. 显式反量化成整块 dense bf16 权重的路径,主要瓶颈不是 LUT 查表本身,而是中间 dense weight 的显存写回和后续再读取。
  3. 如果把 bitsandbytes 小 batch fast path 的 GEMV/GEMM 组织方式迁到自定义 kernel,再把其中的 NF4 解码部分换成自己的版本,确实可以在 decode-like 小 batch 上得到端到端正收益。
  4. 只替换 q_proj 时,真实 vLLM 服务端到端吞吐大约提升 1.6%;扩到 q/k/v/o 后,提升大约 2.5%;单独替 down_proj 基本没有端到端收益。

我是怎么做的

1. 跑通 vLLM + BitsAndBytes 4-bit 推理

实际运行环境在服务器 deep。模型为 unsloth/Qwen2.5-14B-Instruct-bnb-4bit 的本地副本。服务通过两卡、pipeline-parallel-size=2max-num-seqs=1 成功启动并返回结果。

这里需要特别说明:真正跑通的是 PP=2,不是 TP=2。vLLM 日志里显示:

  • rank 0: PP rank 0, TP rank 0
  • rank 1: PP rank 1, TP rank 0

这是因为 vLLM 对 pre-quantized BitsAndBytes checkpoint 不支持 tensor parallel,但允许 pipeline parallel。

2. 梳理 NF4 在 vLLM 链路中的位置

我直接读了 vLLM 安装目录里的 BitsAndBytes 相关代码,重点看了:

  • vllm/model_executor/model_loader/bitsandbytes_loader.py
  • vllm/model_executor/layers/quantization/bitsandbytes.py

结论如下:

  1. loader 会先把 checkpoint 中的量化元数据组装成 QuantState
  2. pre-quantized checkpoint 的 weight.absmaxweight.quant_mapweight.nested_absmaxweight.nested_quant_map 等元数据,会在加载阶段被收集并重建。
  3. _bind_quant_states_to_params() 会把 double-quant 的 scale 元数据恢复出来,也就是把 nested 的 absmax 展开成 float32。
  4. 但 packed 的 4-bit 权重本体仍然保留为 uint8,不会在普通 dense 路径中提前整层展开成 bf16/fp16 常驻显存。
  5. dense 线性层热路径最终走的是 bitsandbytes.matmul_4bit(x, packed_weight.T, quant_state)
  6. MoE 那条路径更接近“先 dequantize_4bit(...),再交给后续算子”。

所以更准确地说,NF4 “恢复”发生在两个层面:

  • 加载阶段:恢复 QuantState 和 double-quant 的 scale 元数据。
  • 推理阶段:普通 dense 层尽量直接消费 packed weight;如果走显式 dequantize_4bit 路径,才会生成完整 dense weight。

3. 先做服务 benchmark

我写了一个 20 请求的流式 benchmark,统计:

  • 首 token 延迟 TTFT
  • 平均端到端耗时
  • completion 吞吐
  • 不同 prompt 长度下的变化

基线结果是:

  • overall avg TTFT: 0.0858 s
  • p50/p95 TTFT: 0.0791 s / 0.1154 s
  • overall completion throughput: 70.76 tok/s

分组后大致如下:

  • short, 约 88 prompt tokens: 0.0793 s, 70.24 tok/s
  • medium, 约 363 prompt tokens: 0.0861 s, 71.47 tok/s
  • long, 约 963 prompt tokens: 0.0986 s, 69.70 tok/s

结论是:低并发条件下,prompt 变长会推高 TTFT,但 decode 吞吐整体比较稳定。

显式反量化路径的实验

实验设计

我没有直接改 bitsandbytes 本体,而是在自己的仓库里做了一套实验性扩展:

  • fs_plugins/custom_ops/nf4_ikko.cpp
  • fs_plugins/custom_ops/nf4_ikko.cu
  • fs_plugins/custom_ops/nf4_ikko.py
  • benchmark_nf4_ikko.py

第一版的目标不是替生产实现,而是把成本拆开量化。具体做法是:

  1. 从 checkpoint 读取 packed 4-bit 权重和量化元数据。
  2. 自己实现 NF4 restore kernel,把 packed weight 恢复成 dense bf16 权重。
  3. 再调用 F.linear
  4. 与默认 bitsandbytes.matmul_4bit 做单层对照。

恢复 kernel 中迁入的优化

后续我又把 mainla.cu 里的关键优化迁到了 restore kernel:

  • shared LUT
  • warp 内广播 scale
  • pair write
  • tail 处理

单层结论

我选了两个代表层做对照:

  • model.layers.0.self_attn.q_proj.weight
  • model.layers.0.mlp.down_proj.weight

结果很一致:restore kernel 本身可以通过优化做快,但“先恢复整层 dense 权重再算”这条路径整体仍然打不过默认 bnb。

down_proj 上,优化后:

  • restore+decode: 0.2600 ms -> 0.2424 ms
  • decode only: 0.2407 ms -> 0.2120 ms

但 restore+linear 总成本仍高于 bnb:

  • batch 1: bnb 0.0314 ms,显式 restore 0.3227 ms
  • batch 8: bnb 0.3399 ms,显式 restore 0.3591 ms

q_proj 上,优化后:

  • restore+decode: 0.1087 ms -> 0.0746 ms
  • decode only: 0.0749 ms -> 0.0415 ms

但总路径依然落后于 bnb:

  • batch 1: bnb 0.0283 ms,显式 restore 0.0596 ms
  • batch 8: bnb 0.0612 ms,显式 restore 0.0686 ms

这里真正慢在哪

结论很明确:显式反量化路径的主要损失不在 LUT 或 scale 恢复,而在:

  1. 把整块 dense bf16 权重写回显存。
  2. 后续 GEMM 又要把这块 dense weight 从显存读一遍。

也就是说,显式 restore 的结构性问题是中间写回开销,而不是 NF4 解码逻辑本身。

从显式 restore 转向 fused decode + matmul

为什么转向 fused

既然问题出在“恢复成整块 dense 再写回显存”,那合理方向就是不再落地 dense weight,而是在 kernel 内边解码边做乘法。

第一版 fused 原型

我先做了一个朴素 fused 原型:

  • 输入 x
  • 输入 packed uint8 weight
  • 输入 quant_state.absmax
  • 在 kernel 里边解码边累加

结果说明方向是对的,但朴素 per-output 累加写法太慢,打不过 bnb。

借 bitsandbytes kernel 组织方式做第二轮迭代

后面我参考了 bitsandbytes 源码里的 kernels.cu,特别是 kgemm_4bit_inference_naive 这类小 batch fast path 的组织方式,把 warp/GEMV 风格迁到了自己的 kernel 里,再把其中的 NF4 解码部分改成自己的实现。

这个版本的核心点是:

  1. 仍然直接消费 packed weight。
  2. 不生成整块 dense weight。
  3. 改成更接近 bnb fast path 的 warp 级输出组织。

修正 packed weight 行偏移错误之后,这条 fused 路径稳定跑通。

单层 fused 对比

q_proj 的 microbenchmark,结果如下:

  • batch 1
    • bnb: 0.0159 ms
    • 显式 restore: 0.0595 ms
    • 新 fused: 0.0129 ms
  • batch 2
    • bnb: 0.0601 ms
    • 显式 restore: 0.0668 ms
    • 新 fused: 0.0227 ms
  • batch 4
    • bnb: 0.0597 ms
    • 显式 restore: 0.0670 ms
    • 新 fused: 0.0413 ms
  • batch 8
    • bnb: 0.0620 ms
    • 显式 restore: 0.0689 ms
    • 新 fused: 0.0787 ms

这说明 fused 方向在 decode-like 小 batch 场景上是成立的:batch 1/2/4 可以赢,batch 8 开始退化。

嵌入 vLLM 真实推理流程

接入方式

我没有直接改 vLLM 安装包,而是用了 monkeypatch:

  • sitecustomize.py
  • vllm_ikko_sitecustomize.py

启动 vLLM 时通过环境变量控制:

  • PYTHONPATH=/home/xyc/PCFG-NAT
  • VLLM_IKKO_ENABLE=1
  • VLLM_IKKO_MODE=<mode>

patch 的是 BitsAndBytesLinearMethod._apply_4bit_weight,即只替换指定层的 4-bit 线性层计算路径,其他层仍走默认 bnb。

支持的模式

目前已经做了这几类:

  • qproj: 只替换 shape == (5120, 5120)q_proj
  • attn: 替换 attention 线相关投影
    • (5120, 5120): q_proj / o_proj
    • (1024, 5120): k_proj / v_proj
  • down_proj: 只替换 shape == (5120, 13824)down_proj

端到端结果

所有端到端对比都使用同一组服务参数:

  • 两卡 PP=2
  • --gpu-memory-utilization 0.6
  • --max-num-seqs 1
  • 20 个串行流式请求
  • 相同 benchmark 脚本和 prompt 组

1. 基线服务

默认 bitsandbytes 路径:

  • avg TTFT: 0.0994 s
  • avg e2e: 0.5774 s
  • throughput: 68.58 tok/s

2. 只替 q_proj

  • avg TTFT: 0.0896 s
  • avg e2e: 0.5684 s
  • throughput: 69.67 tok/s

相对基线:

  • throughput 提升约 +1.6%
  • avg e2e 降低约 -1.6%

3. 扩到 q/k/v/o 四个 attention 投影

  • avg TTFT: 0.0884 s
  • avg e2e: 0.5634 s
  • throughput: 70.28 tok/s

相对基线:

  • throughput 提升约 +2.5%
  • avg e2e 降低约 -2.4%

这说明 attention 线整体替换比只替 q_proj 更有效。

4. 只替 down_proj

我又额外跑了一轮 down_proj 的端到端对比:

  • avg TTFT: 0.0894 s
  • avg e2e: 0.5681 s
  • throughput: 69.70 tok/s

相对基线:

  • throughput 提升约 +1.6%
  • avg e2e 降低约 -1.6%

这个数值和只替 q_proj 很接近,但没有超过 attention 全替换版本。结合前面的单层实验,可以更稳妥地理解为:

  1. down_proj 单层上并不是最适合 decode-like 优化的层。
  2. 端到端里出现的小幅改善,更多是“局部替换仍然能成立”,但收益上限不如 attention 线。
  3. 真正最值得继续扩的仍然是 q/k/v/o 这类更贴近 decode 热路径的层。

补充 Benchmark:固定 Prompt + 5 Repeats

前面的端到端结果是单轮 benchmark,能说明方向,但还不够干净。为了收尾,我又补了一轮更严格的对照:

  1. 固定 20 条请求,不再在不同模式之间重新生成 prompt。
  2. 只保留三组最有代表性的模式:
    • baseline
    • q_proj
    • attnq/k/v/o
  3. 每组都跑 5 轮,统计 mean / std / min / max

这轮 benchmark 使用的是同一份固定 prompt 集 fixed_qwen_prompts.json,每一轮都按完全相同的请求顺序执行。

结果汇总

mode avg TTFT mean ± std (s) avg e2e mean ± std (s) throughput mean ± std (tok/s) throughput min/max
baseline 0.0800 ± 0.0004 0.5605 ± 0.0005 71.01 ± 0.07 70.91 / 71.08
q_proj 0.0811 ± 0.0040 0.5620 ± 0.0029 70.75 ± 0.52 69.84 / 71.04
attn 0.0810 ± 0.0038 0.5619 ± 0.0039 70.76 ± 0.64 69.62 / 71.07

这一轮更干净 benchmark 的结论

这组结果和前面的单轮结果相比,更适合拿来做最终结论。因为它把 prompt 集和重复波动都控制住了。

从这轮数据看:

  1. baseline 的平均吞吐反而略高于 q_projattn 两个自定义路径。
  2. 三组模式之间的差距已经缩到 0.2~0.3 tok/s 量级,远小于 q_proj / attn 自身跨轮波动。
  3. q_projattnstd 明显高于 baseline,说明自定义路径当前还没有展现出更稳定的端到端收益。

所以如果只看这轮“固定 prompt + 5 repeats”的正式 benchmark,应该更保守地下结论:

  1. 自定义 fused 路径已经可以正确嵌入 vLLM 真实推理流程。
  2. 它在单层 microbenchmark 上能在 decode-like 小 batch 场景取得优势。
  3. 但在当前实现水平下,这个优势还没有稳定转化成端到端、统计上更有说服力的收益。

换句话说,前面那组 +1.6% / +2.5% 更适合被理解成“单轮观测到的正向信号”,而不是已经被重复实验充分证实的稳定收益。

Profiling 与结果解释

为了把现象解释清楚,我又补了一轮 profiling。原本想直接用 ncu / nsys 拿硬件计数器,但这台服务器上的 CUDA driver 和 Nsight CLI 版本有兼容性问题,硬件计数器和标准 report 导出都不稳定,所以最后采用的是 torch.profiler 做算子级 CUDA 时间统计。这个方法拿不到 occupancy、L2 hit rate 这类硬件计数器,但足够回答下面三个问题:

  1. batch 8 的退化是不是发生在 fused kernel 本体。
  2. 退化更像 warp 利用率问题,还是更像访存/数据复用问题。
  3. attention 线为什么比 down_proj 更值得继续优化。

1. batch 8 为什么开始退化

先看 fused kernel 本体的 CUDA 时间。下面的数字是 torch.profiler 对 20 次调用统计出的平均单次 CUDA 时间:

  • q_proj, batch 1: 12.354 us
  • q_proj, batch 8: 77.877 us
  • down_proj, batch 1: 29.637 us
  • down_proj, batch 8: 205.781 us

这个结果说明两点:

  1. 退化确实集中在 fused kernel 本身,不是 Python 调度、aten::tcudaLaunchKernel 之类的外围开销。profiler 里最主要的 CUDA 时间全部落在 nf4_fused_matmul_absmax_* 上。
  2. 当前 kernel 在 batch 1/2/4 的工作点上是合适的,但到 batch 8 已经开始从“decode-like 小 batch GEMV”向“小 GEMM”过渡了。现在这版实现仍然沿用偏小 batch 的 warp/GEMV 组织,所以当 M 增大时,对输入激活 x 的复用不够好,batch 维上的工作没有像真正 tiled GEMM 那样被高效摊开。

更直白一点说:batch 8 慢下来,不是 NF4 解码突然出了问题,而是当前 fused kernel 的最优工作区间本来就偏 batch 1/2/4。当 batch 增长后,kernel mapping 开始偏离最优点。

2. 更像 warp 利用率问题,还是寄存器/访存平衡问题

从这轮 profiling 和现有 kernel 结构看,主因更像“访存/数据复用不足”,其次才可能是寄存器压力,而不是单纯的 warp 利用率问题。

依据主要有三条:

  1. 当前 fused kernel 在 q_projbatch 1/2/4 上已经能赢 bitsandbytes,这说明 warp 级输出组织本身不是根本错误。如果 warp 利用率一开始就很差,它不会在这些工作点上取得正收益。
  2. batch 8 的退化是随着 M 增大逐步出现的,更符合“输入 x 和量化权重/scale 的流式读取量增加,而 shared-memory / tile 复用不够”的模式。
  3. profiler 看到的主要增长来自 kernel CUDA 时间本体,而不是 launch 次数或额外算子堆积,这说明问题在 kernel 内部的数据流,而不是外层调度。

可以把当前 fused kernel 理解成:它已经完成了“边解码边乘”的第一步,但还没有做到真正 GEMM-style 的 tile 化。于是它的瓶颈更像:

  • packed weight 和 absmax 仍然偏 streaming 访问
  • x 在 batch 维增大后没有被充分复用
  • 算术强度不够高,更多表现为内存流量随 batch 增大而放大

所以这部分更适合写成:

  • 现阶段更像“访存/数据复用与 kernel mapping 问题”
  • 不是先把锅甩给寄存器或 occupancy

当然,要最终把这个判断坐实,后面还是应该补一轮真正的硬件 profiling,目标指标包括:

  • achieved occupancy
  • registers per thread
  • local memory spill
  • dram throughput
  • L2 hit rate
  • warp stall reason

3. 为什么 attention 线比 down_proj 更值得优化

这部分既能从端到端结果看,也能从 profiling 里看。

先看端到端:

  • 只替 q_proj: 68.58 -> 69.67 tok/s
  • q/k/v/o: 68.58 -> 70.28 tok/s
  • 只替 down_proj: 68.58 -> 69.70 tok/s

attention 线整体替换的收益最高。

再看 profiler 中 fused kernel 的单次 CUDA 时间:

  • q_proj, batch 1: 12.354 us
  • q_proj, batch 8: 77.877 us
  • down_proj, batch 1: 29.637 us
  • down_proj, batch 8: 205.781 us

当前 kernel 对 attention 线更友好,主要有三个原因:

  1. attention 投影更贴近 decode 热路径
    在自回归 decode 里,每一步都会频繁经过 q/k/v/o 这些投影,而且输入 batch 通常很小,这正好落在当前 fused kernel 最擅长的工作区间。

  2. attention 投影的形状更匹配当前 warp/GEMV 风格实现
    当前已经验证过的 attention 形状主要是:

    • (5120, 5120)q_proj / o_proj
    • (1024, 5120)k_proj / v_proj

    这些形状更接近当前 kernel 的小 batch 目标场景。

  3. down_proj 更宽,更容易暴露当前 kernel 的访存问题
    down_proj 的形状是 (5120, 13824),输出更宽,意味着每次调用要流过更多 packed weight 和 scale 数据。由于当前 fused kernel 还没有做真正的 shared-memory tile 复用,这类更宽的矩阵更容易变成内存流量主导。

所以当前阶段最合理的结论不是“down_proj 没价值”,而是:

  1. down_proj 也可以接,而且局部路径是能跑通的。
  2. 但它对 kernel 组织的要求更接近通用 GEMM,而不是 decode-like GEMV。
  3. 在现有实现水平下,attention 线更容易把 fused 路径的优势转成真实端到端收益。

最终判断

NF4 恢复到底发生在哪

  1. 加载阶段恢复的是 QuantState 和 double-quant scale 元数据。
  2. 普通 dense 路径不会先把整层 NF4 权重恢复成 dense bf16 常驻显存。
  3. 如果走显式 dequantize_4bit 路径,才会发生整层 dense weight 的中间写回。

显式反量化值不值得

对 decode-like 小 batch 场景,不值得直接采用“restore -> dense -> linear”这条路径。即使 restore kernel 局部优化有效,中间写回开销仍然会吞掉大部分收益。

什么方向值得继续

当前最值得继续的方向不是进一步优化显式 restore,而是:

  1. 继续做直接消费 packed weight 的 fused decode + matmul。
  2. 优先替 attention 线上的更多层。
  3. 重点服务 decode-like 小 batch,而不是追求一开始就做通用大 GEMM。

这次改了哪些东西

本地仓库中,本轮核心修改集中在这些文件:

  • bench_vllm_qwen.py
  • benchmark_nf4_ikko.py
  • fs_plugins/custom_ops/nf4_ikko.cpp
  • fs_plugins/custom_ops/nf4_ikko.cu
  • fs_plugins/custom_ops/nf4_ikko.py
  • sitecustomize.py
  • vllm_ikko_sitecustomize.py
  • vllm_bnb_benchmark_notes.md
  • outputs/reports/nf4_ikko_experiments.md

其中:

  • nf4_ikko.cu 是 restore kernel 和 fused kernel 的主体。
  • nf4_ikko.py 负责 PyTorch 扩展加载与接口封装。
  • vllm_ikko_sitecustomize.py 负责把自定义 fused 路径嵌到 vLLM 的 BitsAndBytes 线性层计算中。

小结

这轮实验最大的收获不是“证明 NF4 restore kernel 能更快”,而是把问题切清楚了:

  1. 默认 bnb 路径真正占优的关键,不只是 LUT 或解码实现,而是它避免了中间 dense weight 写回。
  2. 只优化 restore 本身,端到端收益有限。
  3. 直接消费 packed weight 的 fused 路径,在真实 vLLM 服务里已经能拿到小幅但稳定的正收益。
  4. 目前最值得继续扩展的是 attention 线,而不是单纯围绕显式反量化做更多微优化。

真实命中点排查

在继续做端到端对比之前,我专门做了一轮“真实调用点排查”。原因是前面我尝试在 BitsAndBytesLinearMethod._apply_4bit_weight 这一层做 timing hook,但 hook 没有命中真实 serve 热路径,所以那一层不适合作为最终 profiling 入口。

排查顺序按下面三层往下走:

  1. vLLM 自己的 BitsAndBytes 包装层
  2. apply_bnb_4bit
  3. bitsandbytes.matmul_4bit

源码链路

从 vLLM 安装目录的源码可以直接确认这条链:

  • _apply_4bit_weight
  • apply_bnb_4bit
  • _apply_bnb_4bit
  • bitsandbytes.matmul_4bit

其中:

  • apply_bnb_4bit 不是普通 Python 函数,而是 torch.ops.vllm.apply_bnb_4bit
  • 它是由 vLLM 把 _apply_bnb_4bit 注册成 custom op 得到的

真实请求命中结果

我直接在安装包源码里给这两层加了最粗粒度探针,只记录:

  • 有没有被调用
  • 输入 shape
  • quantized weight 的 shard shape
  • matmul_4bit 里最终走的是 gemv_4bit 还是 MatMul4Bit.apply

真实请求之后,两个日志文件都稳定命中:

  • /home/xyc/vllm_apply_bnb_4bit_hits.log
  • /home/xyc/bnb_matmul_4bit_hits.log

关键结论如下:

  1. apply_bnb_4bit 确实是 vllm serve 真实 decode 流程中的热路径。
  2. decode 阶段的 A=(1, 5120) / A=(1, 13824) 最终落到了 bitsandbytes.matmul_4bit
  3. 对低 batch decode,matmul_4bit 最终走的是 gemv_4bit fast path,不是 MatMul4Bit.apply 慢路径。
  4. prefill 大 shape(例如 A=(2048, 5120))更接近 MatMul4Bit.apply 这条通用路径。

也就是说,真实 decode 热路径并不是:

  • “先 dequantize_4bit 再通用 linear

而是:

  • apply_bnb_4bit -> bitsandbytes.matmul_4bit -> gemv_4bit

q/k/v/o 的真实 shard 顺序

进一步把 _apply_bnb_4bitquant_states[i] 的真实循环顺序打出来之后,可以确认 attention 线在 decode 阶段的三分支顺序是:

  • q_proj
  • k_proj
  • v_proj

对应的实际 shard shape 是:

  • q_proj: (5120, 5120)
  • k_proj: (1024, 5120)
  • v_proj: (1024, 5120)

而独立的 o_proj 则是单独的:

  • o_proj: (5120, 5120)

这一步很重要,因为它说明后续如果要做 q/k/v/o 的逐项 timing,就应该把上下文从 _apply_bnb_4bit 传到 bitsandbytes.matmul_4bit,而不是继续在更上层的错误入口做统计。

Attention Projection 时间对比

在确认真实命中点之后,我又做了一轮更直接的实验:

  1. q/k/v/o 的上下文从 vLLM 的 _apply_bnb_4bit 传到 bitsandbytes.matmul_4bit
  2. matmul_4bit 这个真实热路径上聚合 decode 阶段的 CUDA 时间
  3. 做两组对比:
    • baseline
    • attn patch

为什么这里用了辅助配置

这里有一个必须说明的点。

如果直接在生产态配置下给 matmul_4bit 内联插入 CUDA event timing,vLLM 的 torch.compile / cudagraph / capture 流程会在 warmup 阶段报错。因此:

  • 调用链的确认,是在生产态配置下完成的
  • 时间聚合,则放到辅助观测配置下完成:--enforce-eager

这个配置不适合拿来代表最终 e2e 性能,但非常适合回答一个更具体的问题:

  • “真实命中点上的 attention projection 时间,到底降了没有?”

baseline vs attn patch

两边都使用同一份固定 prompt 集,并只统计 decode 阶段命中的 q/k/v/o 投影时间。

结果如下,单位都是 ms_per_token_row

projection baseline attn patch
q_proj 0.03298 ms 0.01560 ms
k_proj 0.01964 ms 0.01088 ms
v_proj 0.02114 ms 0.01024 ms
o_proj 0.03620 ms 0.01151 ms

把四项加总得到:

  • baseline per-token projection time: 0.10997 ms
  • attn patch per-token projection time: 0.04824 ms

也就是:

  • 每 token 的 attention projection 总时间下降了 0.06173 ms
  • 降幅约 56.1%

这说明什么

这个结果非常关键,因为它回答了一个此前没有分清的问题:

  1. attention projection 这一项本身,确实明显下降了。
  2. 所以问题不在“kernel 根本没接进去”。
  3. 如果 e2e benchmark 仍然没有稳定改善,那么更大的问题就在系统别处,而不是 attention projection 本身。

换句话说,当前结论应该分两层:

  • kernel / projection 子系统层面:优化是成立的,而且降幅很明显
  • serve 端到端层面:收益还没有稳定穿透整条系统链路

这也把后续方向收窄了:

  1. 继续提升 attention 覆盖率是合理的,但不是唯一问题。
  2. 需要进一步量化非 attention 层、框架调度、prefill 路径以及其他 decode 固定开销。
  3. 如果 projection 已经降了 50% 以上而 e2e 没明显跟上,那真正拖慢整体的部分已经不再是 attention projection 本身。
  • Title: qwen部署
  • Author: Ikko
  • Created at : 2026-03-29 17:23:18
  • Updated at : 2026-04-07 20:10:07
  • Link: http://ikko-debug.github.io/2026/03/29/qwen/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments