本地 27B Q4 + opencode 全程驱动:hami-learning-cloud 教学云更新实录(附模型自评)

五月的《实验室单卡GPU虚拟化:K3s + Z2JH + HAMi》把”K3s + Z2JH + HAMi”的骨架跑通了。这次我们把骨架长成了一个能用的教学云 hami-learning-cloud

比内容更重要的是:这次更新的全部内容——代码审计、功能修复、镜像构建、部署验证、git 历史清洗——完全由本地部署的 Qwen3.8-27B(Q4_K_M 量化,2×RTX 3080 张量并行)+ opencode 完成,全程没有调用任何云模型。 文末让这个模型对自己的表现做了一次自评。

Read More

纸上得来终觉浅,绝知此事要躬行——llama.cpp 部署调优实录

纸上得来终觉浅,绝知此事要躬行。
——陆游《冬夜读书示子聿》

没有调查,就没有发言权。
——毛泽东《反对本本主义》


一、背景

我们用 2 张 RTX 3080 20G 跑 Qwen3.8-27B-GGUF(Q4_K_M 量化),Tensor Parallel 模式(-sm tensor),给 agent 提供推理服务。

网上关于 llama.cpp 的调优文章汗牛充栋,KV Cache 量化、Flash Attention、多 slot 并发——每个参数都被人写过”最佳实践”。但别人的最佳实践,放到你的硬件、你的模型、你的 workload 上,是不是还是最佳? 不试一下,谁也不知道。

于是我们决定:逐个参数实测,用数据说话。

二、初始配置:裸跑

最开始的启动脚本很简单:

1
2
3
4
5
6
llama-server \
-m Qwen3.8-27B-Q4_K_M.gguf \
-ngl 99 -sm tensor \
-c 262144 --parallel 1 \
--spec-type draft-mtp --spec-draft-n-max 2 \
--host 0.0.0.0 --port 8080
  • 没开 Flash Attention(-fa 默认 auto)
  • 没开 KV Cache 量化(-ctk/-ctv 默认 f16)
  • 单 slot(--parallel 1

实测数据:

  • Prefill:~960 tok/s(大 prompt)
  • Generation:~60-70 t/s
  • 显存:17.3G / 20G(GPU 0),15.8G / 20G(GPU 1)

看起来还行?但问题出在多 agent 并发时——单 slot 意味着所有请求排队,一个 agent 在思考,其他 agent 只能干等。

三、第一刀:开 Flash Attention

网上都说 Flash Attention “无脑开就好”。真的吗?

1
llama-server ... -fa on

实测数据(单 slot):

  • Prefill:~967 tok/s(基本持平)
  • Generation:~60 t/s(基本持平)

结论:Flash Attention 在 RTX 3080 上对 prefill 和 generation 速度几乎没有提升。

为什么?因为 Flash Attention 的核心优势是降低 peak memory,而不是加速计算。在 20G 显存的卡上跑 Q4 量化模型,KV Cache 本身就不大(f16 下 262K context 约 2-4G),FA 省下来的那点内存,并没有转化为速度提升。

真正的价值: 如果你要跑更长的 context(比如512K+),FA 能防止 OOM。但在我们的场景下,它更像是一个”保险”,而不是”加速”。

四、第二刀:KV Cache 量化

这是最核心的调优。f16 的 KV Cache 每个元素 2 字节,Q8 量化后只要 1 字节,理论上 KV Cache 显存占用减半。

4.1 尝试 -ctk q8_0 -ctv q8_0

1
llama-server ... -fa on -ctk q8_0 -ctv q8_0

成功启动。 实测:

  • 显存占用变化不大(因为262K context 的 KV Cache 本身就不大)
  • Cache 命中率正常(90%+)
  • 无异常换入换出

4.2 尝试 -ctv q4_0(更激进的量化)

理论上 q4 比 q8 再省一半,我们试了:

1
llama-server ... -ctk q8_0 -ctv q4_0

崩了。 报错:

1
GGML_ASSERT(ret.axis != GGML_BACKEND_SPLIT_AXIS_UNKNOWN) failed

原因:q4_0 量化格式不支持 Tensor Parallel 的跨 GPU 分片。 GGML 后端无法将 q4 的 KV tensor 拆分到两张卡上。

4.3 尝试 -ctv q4_1

1
llama-server ... -ctk q8_0 -ctv q4_1

也崩了。 同样的错误。q4_1 也不支持 TP 分片。

结论:在 -sm tensor 模式下,KV Cache 量化的最低安全选项是 q8_0。q4_0/q4_1 虽然省内存,但和 TP 不兼容。

这是一个典型的”纸上得来”的坑——很多文章推荐 q4 量化 KV Cache,但它们跑的是单卡模式。多卡 TP 下,量化选项受限。

五、第三刀:多 Slot 并发

单 slot 排队太慢,我们加了 --parallel 4

1
llama-server ... --parallel 4 --kv-unified

问题出现了: 两个 agent 同时发大 prompt(109K + 76K tokens),4 个 slot 的统一 KV Cache 开始频繁淘汰:

1
2
W srv alloc: making room for prompt cache entry, removing oldest entry (size = 599 MiB)
W srv alloc: making room for prompt cache entry, removing oldest entry (size = 2119 MiB)

Cache 命中率暴跌到 0%——每次请求都要重新 prefill 全部 tokens,因为之前的缓存被挤掉了。

这是”没有调查就没有发言权”的经典案例: 文档说 --parallel 是”总 context 除以 slot 数”,但没人告诉你:如果两个 agent 同时发大 prompt,总 KV Cache 不够用时会发生什么。答案是:互相挤兑,全部重算。

解决方案

降为 --parallel 2,减少并发 slot 数,给每个 agent 留足 KV Cache 空间:

1
2
llama-server ... --parallel 2 --kv-unified \
-ctk q8_0 -ctv q8_0 -fa on

实测数据:

  • Cache 命中率:98-99%
  • Cache 淘汰:0 次
  • Generation:~61 t/s(单 slot 独占时)
  • 两个 slot 同时活跃时:~35 t/s(合理,显存带宽被瓜分)

六、最终配置与经验总结

最终启动脚本

1
2
3
4
5
6
7
8
9
10
11
llama-server \
-m Qwen3.8-27B-Q4_K_M.gguf \
--mmproj mmproj-BF16.gguf \
-ngl 99 -sm tensor \
-c 262144 --parallel 2 \
--spec-type draft-mtp --spec-draft-n-max 2 \
-fa on \
-ctk q8_0 -ctv q8_0 \
--image-min-tokens 1024 \
--kv-unified \
--host 0.0.0.0 --port 8080

参数调优对照表

参数 我们的尝试 结论
-fa on ✅ 开启 对速度无明显提升,但防止长 context OOM,建议开着
-ctk q8_0 ✅ 开启 KV Cache K 用 Q8,省 50% 显存,推荐
-ctv q8_0 ✅ 开启 KV Cache V 用 Q8,和 TP 兼容的最低选项
-ctv q4_0/q4_1 ❌ 崩溃 -sm tensor 不兼容,多卡勿用
--parallel 4 ⚠️ 挤兑 4 slot 在大 prompt 下互相淘汰,命中率暴跌
--parallel 2 ✅ 稳定 2 slot 是我们的最佳平衡点
--kv-unified ✅ 开启 统一 KV Cache 池,比 per-slot 更灵活

踩坑记录

  1. -fa 不需要显式 on 错。-fa 单独用会把下一个参数当值解析(-fa -ctk 会把 -ctk 当成 flash-attn 的值),必须写 -fa on

  2. --prompt-cache-all 不存在? 对。这个参数在某些文章里出现过,但我们的 build 版本没有。实际的 prompt caching 是 --cache-prompt,默认就是开启的。

  3. q4 量化 KV Cache 能省一半显存? 在单卡上可以。在多卡 TP 模式下不行,GGML 后端不支持对 q4 tensor 做跨 GPU 分片。

  4. 多 slot 就是好? 不一定。slot 越多,KV Cache 池被瓜分越厉害。大 prompt 场景下,少 slot 反而更稳。

七、方法论:没有调查就没有发言权

这次调优最大的收获不是参数配置,而是方法论:

1. 不要迷信”最佳实践”

每篇文章都说”开 Flash Attention”、”用 Q4 量化 KV Cache”。但在我们的硬件(2x RTX 3080)和模式(TP)下,这些”最佳实践”要么无效,要么直接崩溃。别人的结论,要在你自己的环境里验证。

2. 用数据说话,不要靠感觉

“感觉”开了 FA 会快一点,”感觉”q4 比 q8 好。但实测数据告诉你:FA 对速度几乎没影响,q4 在 TP 下根本跑不了。没有数据支撑的优化,就是自欺欺人。

3. 监控比调参更重要

调完参数不是结束,而是开始。我们用 /slots API 监控 cache 命中率、用 nvidia-smi 看显存、用 log 看淘汰事件。不监控的优化,等于盲人摸象。

4. 逐步调整,不要一次改太多

我们先加 FA,测一轮;再加 Q8,测一轮;再改 parallel,测一轮。如果一次改三个参数,出了问题你都不知道是哪个参数的锅。控制变量,逐步推进。

八、结语

纸上得来终觉浅,绝知此事要躬行。

写代码如此,调参数亦如此。llama.cpp 的每个参数背后都有复杂的行为,文档和文章只能给你一个起点,真正的理解必须来自你自己的实测。

这次调优从”裸跑”到”FA + Q8 + 2 slot”,每一步都踩过坑、看过数据、做过判断。最终的配置不一定是最优的,但它是我们调查过、验证过、理解过的。

这,才是”躬行”的意义。


本文记录了 2026 年 8 月在 2x RTX 3080 20G 上部署 Qwen3.8-27B 的 llama.cpp 调优过程。硬件、模型、workload 不同,结论可能不同。请以实测为准。

What Happens When You Increase num_warps in Triton — 寄存器压力的实证调查

动机

在编写 Triton kernel 时,num_warps 是最常调节的编译参数之一。直觉上,更多的 warp 意味着更高的 occupancy,应该提升性能。但在某些场景下,增加 num_warps 反而会导致显著的性能回退——非单调的寄存器压力崩溃

本文通过一系列实验,系统调查了以下问题:

  1. num_warps=16 到底比 num_warps=2 慢多少?
  2. 变慢的物理机制是什么?是传统的 local memory spill,还是别的什么?
  3. 这个现象是 cherry-pick 的巧合,还是普遍存在的?
  4. Ampere 和 Blackwell 两代架构有何差异?
  5. 有没有办法故意触发真正的 STL/LDL spill?

所有代码和实验均在以下环境完成:

  • GPU: NVIDIA GeForce RTX 3080 (SM 8.6, Ampere) + RTX 5060 Ti (SM 12.0, Blackwell)
  • PyTorch: 2.11.0 + CUDA 13.0
  • Triton: 3.6.0
  • ptxas: Triton 捆绑版 (CUDA 2025)

代码全文见文末。

Read More

macOS NT QQ 聊天记录解密

背景

最近出于兴趣研究一下 macOS 上新版 NT QQ 的聊天记录存储机制。发现其使用了加密的 SQLite 数据库来存储消息数据。

环境信息

  • 系统: macOS
  • 软件: NT QQ (新版腾讯 QQ)
  • 数据库类型: SQLCipher (加密 SQLite)
  • 加密算法: AES-256 + HMAC-SHA1

数据定位

NT QQ 的用户数据主要存储位置:

1
~/Library/Application\ Support/QQ/

在这个目录下可以找到两个关键的数据库文件:

  1. nt_msg.clean.db - 主消息数据库(约 2.8GB)
  2. profile_info.db - 用户配置信息(约 9.8MB)

密钥获取

NT QQ 将解密密钥存储在一个文本文件中,通常命名为类似 @qqkey 的文件。通过查看该文件可以找到数据库的解密密码。

注意: 不同版本的 QQ 可能使用不同的密钥存储方式和位置。

解密过程

1. 导出 SQL Dump

使用 sqlite3 命令行工具配合正确的加密参数导出数据:

1
2
3
4
5
6
7
8
9
sqlcipher nt_msg.clean.db <<EOF
PRAGMA key = '<实际密码已隐去>'; # 从密钥文件获取的密码
PRAGMA kdf_iter = 4096; # QQ 使用的 KDF 迭代次数
PRAGMA cipher_hmac_algorithm = HMAC_SHA1;
PRAGMA cipher_page_size = 4096; # 默认页面大小
.output raw_dump.sql
.dump
.exit
EOF

2. 修复 SQL Dump

由于数据库可能处于事务状态,导出的 SQL 文件会以 ROLLBACK 结尾,需要替换为 COMMIT

1
cat raw_dump.sql | sed -e 's|^ROLLBACK;\( -- due to errors\)*$|COMMIT;|g' | sqlite3 nt_msg_fixed.db

3. 读取解密后的数据

1
sqlite3 nt_msg_fixed.db ".tables"

可以看到多个数据表,主要包括:

1
2
3
4
5
6
c2c_msg_table              # 私聊消息表
group_msg_table # 群聊消息表
dataline_msg_table # 长消息
recent_contact_v3_table # 最近联系人
c2c_temp_msg_table # 临时会话消息
discuss_msg_table # 讨论组消息

注意事项

  1. PRAGMA kdf_iter: NT QQ 使用的是 4096 次迭代,而非 SQLCipher 的默认值 4000。这点很重要,如果参数不对会导致解密失败。
  2. 事务处理: 导出的 SQL dump 可能包含未提交事务,需要手动修复。
  3. 数据完整性: 建议先将解密后的数据库备份再进行查询操作。

结语

通过以上步骤,就可以成功解密 NT QQ 的聊天记录并导出数据。整个过程中最重要的是获取正确的加密密钥和配置参数。这个研究过程让我对 Qt 应用的数据存储机制有了更深的理解。


免责声明: 本文仅用于技术学习和个人数据备份,请勿用于非法用途或侵犯他人隐私。

cuasmrl 部署实录:用 RL 优化 CUDA Kernel 指令调度

cuasmrl 部署实录:用 RL 优化 CUDA Kernel 指令调度

背景

cuasmrl 是一个基于 TritonCuAssembler 的研究项目,核心思路是:

用强化学习(PPO)对 Triton 编译生成的 SASS 指令进行指令级重排序,通过调整 memory instruction 的相对位置来隐藏 latency,提升 kernel 的实际吞吐。

整个 pipeline 大致是:

1
2
3
Triton JIT 编译 → cubin → CuAssembler 反汇编为 SASS
→ 静态分析 (stall count / 依赖图) → RL agent 重排指令
→ CuAssembler 重新汇编为 cubin → 加载执行 → 测量 TFLOPS → reward

最近在一台双卡服务器上成功复现了这套流程,记录一下踩过的坑。

Read More

实验室单卡GPU虚拟化:K3s + Z2JH + HAMi 实现多用户GPU共享

背景

实验室有一台 GPU 服务器(RTX 3080 + RTX 5060 Ti,共两张卡),之前多个人要用 GPU 只能排队——一个人独占整张卡,其他人等着。这种粗放模式显然效率太低,尤其是跑 Jupyter Notebook 做实验的时候,大部分时间 GPU 根本跑不满。

目标很明确:

  • 一台物理机,两张消费级 GPU
  • 多个用户同时使用 JupyterHub 做 PyTorch 开发
  • 每个用户分到 4GB 显存 + 25% GPU 算力
  • 一张卡最多切 5 个 vGPU,总共支撑 10 个用户并发
  • 能可视化监控 GPU 使用情况

最终效果:一个 Helm values 文件 + 一套自动化安装脚本,在 K3s 上把 Z2JH、HAMi、HAMi-WebUI 全栈跑起来。


架构总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──────────────────────────────────────────────────────────────┐
│ Ubuntu 24.04 LTS │
│ K3s (轻量 Kubernetes) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ HAMi — GPU 虚拟化中间件 │ │
│ │ ├─ hami-scheduler # 自定义 GPU 感知调度器 │ │
│ │ ├─ hami-device-plugin # 注册 vGPU 资源到 K8s │ │
│ │ └─ webhook # 自动注入 GPU 环境变量 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Z2JH (JupyterHub) │ │
│ │ ├─ Hub # 用户认证 & 管理 │ │
│ │ ├─ Proxy # NodePort │ │
│ │ └─ User Pods # PyTorch CUDA Notebook │ │
│ │ # 每个 Pod: 4GB显存 / 25%算力 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ HAMi-WebUI (魔改版) │ │
│ │ ├─ 前端: Vue.js / NodePort │ │
│ │ ├─ 后端: Go (Kratos) │ │
│ │ └─ Prometheus + DCGM-Exporter 监控 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ GPU: RTX 3080 + RTX 5060 Ti │
└──────────────────────────────────────────────────────────────┘

核心技术选型

K3s 替代标准 K8s

标准 K8s 太重了——kube-apiserver、etcd、controller-manager、scheduler 一堆组件,单机跑起来吃掉好几 GB 内存。我们是单节点场景,用 K3s 完美替代:

  • 二进制只有 ~70MB,内存占用不到 500MB
  • 自带 containerd、Flannel CNI
  • 完全兼容标准 K8s API,Helm 直接可用
  • 禁用了 traefik ingress,用 NodePort 直出端口更简单

HAMi 做 GPU 虚拟化

HAMi(异构算力管理中间件,原 vGPU-device-plugin)是开源的 GPU 共享调度方案,核心能力:

功能 说明
显存切分 一张物理 GPU 按 MB 级别切分,deviceSplitCount: 10 最多切 10 份
算力共享 deviceCoreSharing: 1,按比例分配 GPU SM 算力
调度器 自定义 scheduler + admission webhook,binpack 策略优先塞满同一张卡
多厂商 除了 NVIDIA,还支持华为 Ascend、海光 DCU、寒武纪 MLU 等

用户 Pod 只需声明三个资源:

1
2
3
4
5
resources:
limits:
nvidia.com/gpu: "1" # 1 个 vGPU
nvidia.com/gpumem: "4000" # 4 GB 显存
nvidia.com/gpucores: "25" # 25% 算力

HAMi webhook 会自动注入 NVIDIA 可见设备环境变量,scheduler 把 Pod 调度到有足够剩余资源的 GPU 切片上。

Z2JH 做用户界面

JupyterHub 是学术界最流行的多用户 Notebook 方案,Z2JH 是它的 Helm Chart。关键配置:

1
2
3
4
5
6
7
8
9
10
singleuser:
extraResource:
limits:
nvidia.com/gpumem: "4000" # 4 GB 显存
nvidia.com/gpucores: "25" # 25% 算力
extraPodConfig:
runtimeClassName: nvidia # 指定 NVIDIA runtime
scheduling:
userScheduler:
enabled: false # 用 HAMi 调度器

认证插件使用 Dummy Authenticator,适合实验室内网环境。


魔改 HAMi-WebUI

原生的 HAMi-WebUI 有个 bug:GPU 使用率永远显示为 0。

Bug 根因

HAMi scheduler 在 Pod annotation 里编码 GPU 分配信息时,是按所有容器(包括 init containers)的顺序写入的。但 WebUI 解码时只遍历了 pod.Spec.Containers,忽略了 init containers,导致索引错位——应用容器拿到的永远是 init container(block-cloud-metadata)的空设备记录。

1
2
3
4
5
Scheduler 写入 annotation:
[init-container设备(空)] ; [主容器设备(GPU-xxx,NVIDIA,4000,25)]

WebUI 解码 (修复前):
i=0 → pod.Spec.Containers[0] → 错误地取了 init-container 的空记录 ❌

修复方案

修了三个文件:

1. util.go — 解码时考虑 init containers

1
2
3
4
5
// 修复前:只算 Containers
totalContainers := len(pod.Spec.Containers)

// 修复后:InitContainers + Containers
totalContainers := len(pod.Spec.InitContainers) + len(pod.Spec.Containers)

2. pod.go — 分配 device 索引时跳过 init containers

1
2
3
4
5
6
7
8
9
// 修复前:直接用 i 作为索引,错位
c.ContainerDevices = bizContainerDevices[i]

// 修复后:加上 initContainer 的偏移量
initContainerCount := len(pod.Spec.InitContainers)
deviceIdx := initContainerCount + i
if deviceIdx < len(bizContainerDevices) {
c.ContainerDevices = bizContainerDevices[deviceIdx]
}

3. node.go — 添加 nil 防护

当 POST 请求不带 filters 时 req.Filters 为 nil,会触发 panic。加上判空:

1
2
3
4
if filters != nil {
if filters.Ip != "" && filters.Ip != nodeReply.Ip { continue }
// ...
}

修复后 WebUI 正确显示各 GPU 使用率、显存和算力占用。


部署流程

整套部署压缩到三个脚本里:

1
2
3
4
5
6
7
8
# 1. 安装 K3s + Helm + 部署 Z2JH
./install-z2jh.sh

# 2. 查看状态
./status-z2jh.sh

# 3. 卸载(含数据清理)
./uninstall-z2jh.sh

HAMi 和 HAMi-WebUI 通过 Helm 单独部署:

1
2
helm install hami HAMi/charts/hami -n kube-system
helm install hami-webui HAMi-WebUI/charts/hami-webui -n kube-system

访问方式

服务 地址 说明
JupyterHub http://<服务器IP>:31080 按配置认证登录
HAMi-WebUI http://<服务器IP>:32361 GPU 资源监控面板

资源分配示意

1
2
3
4
5
6
7
8
RTX 3080 (10GB)          RTX 5060 Ti (16GB)
┌─────────────────┐ ┌─────────────────┐
│ vGPU 1: 4GB 25% │ │ vGPU 6: 4GB 25% │
│ vGPU 2: 4GB 25% │ │ vGPU 7: 4GB 25% │
│ vGPU 3: 2GB 25% │ │ vGPU 8: 4GB 25% │
│ │ │ vGPU 9: 4GB 25% │
└─────────────────┘ └─────────────────┘
5 个 vGPU 切片 5 个 vGPU 切片

每个 JupyterHub 用户 Pod 自动分配到一个 vGPU 切片,跑 PyTorch 和独占一张卡体验完全一致——torch.cuda.is_available() 返回 true,nvidia-smi 能看到 GPU,只不过显存上限是 4GB。


总结

这套方案的核心价值:

  1. 消费级 GPU 也能虚拟化——不需要 Tesla/V100 这类数据中心卡
  2. 资源利用率大幅提升——从一人占整卡到十人共享
  3. 开箱即用——K3s 单机一条脚本拉起全栈
  4. 可观测——WebUI 实时看 GPU 占用,Prometheus 留存历史数据
  5. 易维护——Helm 统一管理,卸载一条命令清理干净

适合实验室、小团队、教学机房等预算有限但需要多用户 GPU 环境的场景。

AMD uProf 性能分析:矩阵乘法优化之旅

背景

CS210 课程的第二次作业要求使用性能分析工具对矩阵乘法程序进行 profiling。由于使用的是 AMD CPU,我选择了 AMD uProf 的 data_access 模式来分析 L1/DTLB 性能变化。

环境

  • CPU: AMD (Family 0x17, Model 0x71, 24 核)
  • OS: Linux Ubuntu 25.04, Kernel 6.14.0-37-generic
  • 编译器: GCC 14.2.0, -g -O3 -march=native
  • 分析工具: AMD uProf 5.3.518 (data_access config)
  • 矩阵大小: 2048×2048

测试用例

使用 Intel oneAPI 的 matrix_multiply_c 示例,包含 5 个优化版本:

版本 函数 优化策略
0 multiply0 基础串行 (i-j-k 循环顺序)
1 multiply1 多线程 (pthreads, 同 i-j-k 顺序)
2 multiply2 循环交换 (i-k-j 顺序)
3 multiply3 循环交换 + 向量化提示 (#pragma ivdep)
4 multiply4 Cache blocking (64 元素块) + 循环展开

编译与分析

每个版本独立编译并使用 AMDuProfCLI 进行数据访问分析:

1
2
3
4
5
6
7
8
9
10
# 编译
gcc -g -O3 -march=native -DUSE_THR -c src/multiply.c -D_LINUX
gcc -g -O3 -march=native -DUSE_THR src/*.c -o matrix -lpthread -lm

# 使用 data_access 模式进行 profiling
AMDuProfCLI collect --config data_access -o profile_multiply0/multiply0-da ./matrix

# 生成 CSV 报告
AMDuProfCLI report -i profile_multiply0/multiply0-da \
--report-output profile_multiply0/multiply0-da-report.csv

结果

性能对比

版本 执行时间 (s) MFLOPS 加速比
multiply0 127.78 134
multiply1 6.89 2,495 18.5×
multiply2 0.77 22,320 166×
multiply3 0.73 23,605 175×
multiply4 0.15 112,524 836×

L1 数据缓存分析

版本 CPI L1 DC Miss Ratio L1 DTLB Miss Rate
multiply0 6.26 16.9% 0.106
multiply1 5.32 26.5% 0.112
multiply2 3.13 26.5% 0.001
multiply3 3.08 24.6% 0.001
multiply4 0.82 23.3% 0.019

关键发现

1. 循环交换是最关键的优化

原始代码中,内层循环访问 b[k][j],其中 k 变化时会跨越整行内存(步长为 2048 个 double = 16KB)。这导致:

  • 大量 L1 缓存未命中
  • 高 DTLB 未命中率(跨页访问)

循环交换为 i-k-j 顺序后,b[k][j] 变为连续内存访问,DTLB 未命中率从 0.106 降至 0.001。

2. Cache Blocking 在大矩阵下效果显著

当矩阵大小为 2048×2048 时(每个矩阵 32MB,共 96MB),工作集远超 L1 缓存但可放入 L2。Cache blocking 将计算限制在 64×64 的块内,使工作集完全驻留在 L2 中:

  • CPI 从 6.26 降至 0.82
  • DRAM refill 从 6.21% 降至 0.01%

3. 多线程不能解决缓存问题

multiply1 虽然通过 16 线程获得了 18.5× 加速,但 L1 DC miss ratio 反而从 16.9% 恶化到 26.5%,因为多线程间的缓存竞争加剧了问题。

AMD Zen3 架构启示

在 AMD Zen3 架构上,矩阵乘法的性能瓶颈主要是:

  1. L1 数据缓存未命中 - 由非连续内存访问模式引起
  2. DTLB 未命中 - 跨页访问导致 TLB 压力
  3. CPI 过高 - 内存停顿导致流水线空闲

最有效的优化策略是 Cache blocking + 循环交换,将工作集限制在 L2 缓存内,同时保证块内访问的连续性。