

HPCGames 题解 A B C 题
最近参加了HPCGames比赛,写下这篇Blog记录一下自己对A B C题的理解与题解
A 题#
首先第一题是签到题,题面给出了一段位置语言的代码,让我们尝试编译并猜测其输出结果。题面代码如下:
program quine
implicit none
integer :: i
character(len=1), parameter :: q = char(34), k = char(44)
character(len=80), dimension(21) :: s
s = [ character(len=80) :: &
"program quine", &
" implicit none", &
" integer :: i", &
" character(len=1), parameter :: q = char(34), k = char(44)", &
" character(len=80), dimension(21) :: s", &
" s = [ character(len=80) :: &", &
" ]", &
" do i = 1, 6", &
" print '(a)', trim(s(i))", &
" end do", &
" do i = 1, 21", &
" if (i < 21) then", &
" print '(5a)', ' ', q, trim(s(i)), q, k // ' &'", &
" else", &
" print '(5a)', ' ', q, trim(s(i)), q // ' &'", &
" end if", &
" end do", &
" do i = 7, 21", &
" print '(a)', trim(s(i))", &
" end do", &
"end program quine" &
]
do i = 1, 6
print '(a)', trim(s(i))
end do
do i = 1, 21
if (i < 21) then
print '(5a)', ' ', q, trim(s(i)), q, k // ' &'
else
print '(5a)', ' ', q, trim(s(i)), q // ' &'
end if
end do
do i = 7, 21
print '(a)', trim(s(i))
end do
end program quineplaintext这道题观察代码并询问AI可知,这是一个Fortran语言的Quine程序,通过阅读代码可知,其具体实现逻辑是:
- 定义一个字符串数组
s,其中包含了程序的每一行代码。 - 使用三个循环来打印程序的各个部分:
- 第一个循环打印程序的前六行代码。
- 第二个循环遍历字符串数组
s,将每一行代码格式化为带引号和逗号的形式,并打印出来。 - 第三个循环打印程序的剩余部分代码。 综上所述,这段代码的输出结果就是它本身的完整代码,实现了Quine程序的特性。
因此可以直接把这段代码复制粘贴作为答案提交即可。
B 题#
这里又来到了经典的小北问答环节,结合一些理论知识和具体论文的查阅,我们可以对题目进行详细的分析和解答。
1. Amdahl & Gustafson#
某程序的代码中 10% 必须串行执行,90% 可完美并行。
- 根据 Amdahl’s Law,无论核心数如何增加,该程序的理论最大加速比极限是 ____ 倍;
- 若在 10 核系统中通过扩大问题规模来保持每核计算负载不变,根据 Gustafson’s Law,该系统的加速比将达到 ____ 倍。
2. OpenMP#
以下代码使用 OpenMP 并行执行循环:
int sum = 0;
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
sum += i;
}
printf("sum = %d\n", sum);c关于该代码,请问以下说法中正确的是 ____ 。
| 选项 | 描述 |
|---|---|
| A | 代码一定能正确计算出 0 到 99 的和(4950) |
| B | 代码存在数据竞争, 结果不确定。 |
| C | sum变量默认为private,每个线程有自己的副本。 |
| D | OpenMP 会自动为sum变量添加原子操作,保证结果正确。 |
3. 低精度#
已知 IEEE 754 标准的 FP32 拥有 8 位指数位。请问:
- BF16 拥有 ____ 位指数位,____ 位尾数位
- NVFP4 拥有 ____ 位指数位,____ 位尾数位
提示:可以查阅资料,了解 NVFP4 如何在低精度下保持较高的数值范围和动态范围。
4. MPI 通信#
4.1 基本原语#
4 个进程执行以下代码,每个进程有局部值 local_val,操作后每个进程都有所有进程的值。
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
int local_val = rank; // rank 为进程编号,0~3
int recv_buf[4];
/* 填这一行代码 */c4.2 通信器#
创建一个 2 维笛卡尔拓扑,尺寸为 2×2,行优先排列,允许环绕连接。
MPI_Comm comm_cart;
int dims[2] = {2, 2};
int periods[2] = {1, 1}; // 环绕连接
/* 填这一行代码 */c5.NCCL 延迟#
在深度学习的并行推理与训练中,进程之间会频繁进行集合通信操作。NVIDIA 的开源集合通信库 NCCL,提供了在 GPU 之间进行集合通信的高性能解决方案。
当在异构的硬件上进行大规模集合通信时,如何选择通信的算法将很影响集合通信操作的效率。为了解决这个问题,NCCL 的解决方案是:基于一套硬编码的调优常数,估算不同集合通信算法下的集合通信完成时间,由此选择最优的算法。
问题:在 NCCL 2.28 的默认调优常量中,用 NVLink 连接的两 GPU、采用 Tree 算法和 LL 协议时,在估算时每跳(单步)的硬件延迟取值为 ______ µs。
6. 高性能网络#
Rail-optimized networking 与 Clos 都是高性能网络设计方案。以下说法正确的有:
| 选项 | 描述 |
|---|---|
| A | 在 Rail-optimized 网络中,来自不同 HB 域(High-Bandwidth Domain)但具有相同 local rank 的 GPU 会被连接到同一个 rail switch 上,以减少跨域通信的延迟 |
| B | 常见部署模式下,Rail-optimized 网络相比传统 Clos 网络的主要优势是完全不需要 Spine 层交换机,因此可以大大节省网络设备成本 |
| C | Clos 网络因其使用 Spanning Tree Protocol (STP) 而在大规模部署时存在扩展性问题,这是 Rail-optimized 网络要解决的核心问题之一 |
| D | Rail-optimized 网络保证了任何情况下集群内任意两个 GPU 之间都能以网络线速(如 400 Gbps InfiniBand)进行通信,无论它们是否在同一个 rail 中 |
| E | NCCL 2.12 引入的 PXN 特性可以结合 NVLink 和 PCI 通信来优化网络流量,这个优化对于 Rail-optimized 网络尤为重要 |
| F | 对于 LLM 训练工作负载,最优的通信策略会将大部分网络流量集中在相同 local rank 的 NIC 之间,并且会多用 NVLink 等高速互联进行跨 rail 交换,这使得 Rail-optimized 架构特别适合此类场景 |
7. GPU#
NVIDIA 的 Hopper 架构引入了 TMA(Tensor Memory Accelerator) 以提升 GPU 内存访问效率。以下说法正确的有:
| 选项 | 描述 |
|---|---|
| A | 相比 cp.async,TMA 可以直接将数据从全局内存加载到共享内存,无需经过寄存器中转,从而能节省寄存器 |
| B | 在 cutlass 的异步流水线抽象中,Producer 调用 producer_acquire 获取空闲的 buffer stage,完成数据加载后调用 producer_commit 通知 Consumer;Consumer 则通过 consumer_wait 等待数据就绪,使用完毕后调用 consumer_release 释放 buffer |
| C | 在使用 TMA 进行数据传输时,所有参与的线程都需要执行相同的 TMA 指令,TMA 硬件会自动处理线程间的协调 |
| D | Cutlass Pipeline 使用多级缓冲(multi-stage buffering),通过 PipelineState 追踪当前读写的 stage index 和 phase,实现 Producer 和 Consumer 之间的流水线重叠 |
| E | TMA 的 multicast 功能允许一次 TMA 操作将同一块数据广播到 Cluster 内的多个 Thread Block 的共享内存中,减少了重复的全局内存访问 |
| F | TMA 描述符(TMA Descriptor)需要在 kernel 启动前在 host 端创建,描述符中包含了张量的形状、步长和 swizzle 模式等信息,kernel 执行时通过预取描述符(prefetch_tma_descriptor)来减少首次 TMA 操作的延迟 |
8. LLM#
对于参数如下的一个标准的 Transformer-Decoder 模型,所有的 all reduce 操作都使用 ring all reduce。假设一共有 4 张卡。
模型参数#
| 参数 | 值 |
|---|---|
| 层数 | 32 层 |
| 隐藏层维度 (h) | 4096 |
| FFN 结构 | 两层线性层,中间层维度为 4h |
| 序列长度 | 2048 |
| Batch Size | 32 |
| 优化器 | Adam + 混合精度训练 |
| 精度设置 | 参数和梯度使用 fp16,Adam 优化器状态使用 fp32(包括 momentum、variance 和 master weights) |
问题#
请计算在以下三种并行方式下,进行一个 batch 的前向传播和反向传播,每张卡需要的发送量(以 GB 为单位):
-
数据并行:每张卡上存放完整的模型,把 batch 均匀拆分到每张卡上,分别计算完成后对梯度进行 All-Reduce 操作
-
流水并行:按层拆分模型放到不同卡上,只需要前向传播的时候发送 activation,反向传播的时候发送 gradient。(计算通信量时只考虑中间的卡)
-
张量并行:对于 MHA 操作,按照 head 拆分到不同卡上。对于 FFN,第一个线性层按照输出维度进行拆分,第二个线性层按照输入维度进行拆分
9. UB 互联#
在高性能计算系统中,集合通信(Collective Communication)的性能主要受带宽(Bandwidth) 与延迟(Latency) 两个因素制约。
NVIDIA 通过 NVLink 与 NVSwitch 构建 GPU 间的高速 Scale-up 互联网络,而华为则提出了 Unified Bus(UB)协议,作为面向 NPU 的统一互联与内存访问机制。UB 协议基于华为自研的 UB Switch 交换芯片,并通过高带宽物理链路 HCCS(High-Capacity Coherent System) 进行连接。
传统 AI 集群通常以 8 卡服务器为基本单元进行 Scale-out 扩展,而华为在 CloudMatrix 384(CM384) 架构中,通过两级 UB Switch 组网,将 384 颗昇腾 910C NPU 构建为一个统一的超节点(SuperPod)。在该超节点范围内,所有 NPU 均处于同一个低延迟的轨道优化网络中,实现全对等 Scale-up 互联。
CM384 进一步将 UB 网络划分为 7 个相互独立的物理平面。每颗 NPU 的 7 个 HCCS 接口分别接入不同的交换平面,从而保证大规模并行通信过程中,数据流在物理路径上完全隔离、无链路冲突。
问题#
在 CloudMatrix 384 的标准满配部署方案中,为了支撑 384 颗昇腾 910C NPU 实现无收敛、全对等的 Scale-up 互联,系统采用两级交换架构。在该超节点的物理拓扑中,分别使用了:
- ____ 个 Level 1 UB Switch
- ____ 个 Level 2 UB Switch
- 最终实现了理论上 ____ GB/s 的系统级聚合带宽
假设 switch chip 提供的单个 Port 可以提供 28GB/s 的通信带宽
10. Cache 行为分析#
假设我们需要进行一个矩阵乘法 。
测试环境
为了简化分析,假设:
| 参数类型 | 配置 |
|---|---|
| 数据类型 | double (8 Bytes) |
| L1 Cache 大小 | 4KB (4096 Bytes) |
| 相联度 | 直接映射 (Direct Mapped, E=1) |
| 块大小 | 64 Bytes(1 个 Cache Line 可存 8 个 double) |
| 矩阵规模 | A,B,C 均为 64×64 的方阵 (N=64) |
| 存储方式 | 数组按行优先存储 |
| 内存对齐 | A,B,C 的起始地址均对齐到 Cache 的起始 Set |
代码实现
// 假设变量 sum 已优化到寄存器中,忽略 C 的访存影响
// 仅考虑内层循环中 A 和 B 的读取
for (int j = 0; j < 64; ++j) { // Loop 1
for (int i = 0; i < 64; ++i) { // Loop 2
double sum = 0.0;
for (int k = 0; k < 64; ++k) { // Loop 3
sum += A[i][k] * B[k][j];
}
C[i][j] = sum;
}
}cpp问题 10.1
我们试图分析上述代码中最内层循环 Loop 3 对矩阵 的访存行为。
已知 Cache 总共有 个 Set。
在计算 的过程中(即一次完整的 Loop 3),关于 的 Cache Miss Rate(不命中率),下列说法正确的是:
| 选项 | 描述 |
|---|---|
| A | 12.5% - 这里有良好的空间局部性,每 8 个 double 只有 1 次 Miss |
| B | 25% - 虽然是列优先访问,但 Cache 够大,只有冷不命中 |
| C | 约 50% - A 和 B 互相打架(冲突),导致一半的数据被驱逐 |
| D | 100% - 发生了严重的 Cache Thrashing(抖动),每次读取都是 Miss |
💡 提示:计算一下访问 和 时的内存地址差值(Stride),以及它们映射到的 Set Index 的跨度。
问题 10.2 为了进一步提升矩阵乘法的效率,我们决定使用分块技术。你将矩阵分成了 的小块(Block Size = 8)。
// 8x8 分块优化演示
for (int jj = 0; jj < 64; jj += 8) {
for (int ii = 0; ii < 64; ii += 8) {
for (int kk = 0; kk < 64; kk += 8) {
// 在这里处理 8x8 的子块乘法
for (int j = jj; j < jj + 8; ++j) {
for (int i = ii; i < ii + 8; ++i) {
double sum = C[i][j]; // 简化写法
for (int k = kk; k < kk + 8; ++k) {
sum += A[i][k] * B[k][j];
}
C[i][j] = sum;
}
}
}
}
}cpp针对一个 的 矩阵子块(假设该子块已预加载),在处理该子块内部的计算时,关于其在 L1 Cache 中的状态,下列分析正确的是:
| 选项 | 描述 |
|---|---|
| A | 一个 8×8 的子块大小为 512 Bytes,远小于 Cache 大小,因此完全没有冲突,所有数据都能驻留在 Cache 中 |
| B | 尽管子块很小,但由于 B 的原始列宽(Stride)很大,导致子块内的 8 行数据全部映射到了同一个 Set 中,依然存在严重的冲突 |
| C | 子块内的 8 行数据分别映射到了 8 个不同的 Set 中(Set 索引间隔为 8),且在子块计算期间不会发生自我冲突(Self-Conflict) |
| D | 分块主要是为了利用 L2/L3 Cache,对这么小的 L1 Cache (4KB) 来说,8×8 的分块没有任何意义 |
C 题#
Ticker#
背景#
在分秒必争的高频交易领域,系统的响应速度直接决定了盈亏。我们的核心交易引擎在最新的鲲鹏服务器上部署后,发现了一个令人困惑的现象:虽然我们为每个交易对分配了独立的计算核心,理论上应当实现完美的线性加速,但实际吞吐量却远低于预期,甚至不如少用几个核心时的表现。
作为团队的首席性能架构师,你需要深入底层,找出阻碍性能释放的元凶,并对关键数据结构进行重构,让这台多核怪兽展现出它在这个并行任务上应有的爆发力。
任务描述#
程序包含两个文件:
main.cpp:负责启动 16 个 OpenMP 线程,生成随机模拟行情,并统计最终结果。
market.h:定义了核心数据结构 Candle 。
你需要修改 market.h 中的结构体定义,以提升程序在多核环境下的运行效率。
修改限制:允许调整结构体的定义方式、内存布局等;禁止修改成员变量的名称(如 high, vol 等),禁止修改变量类型及其精度(必须保持 double/long long)。
请提交修改后的 market.h 文件。
输入输出#
输入 format 程序从标准输入读取数据:
N (long long): 每个线程模拟的 Tick 数。Seed (int): 随机种子。 输出 format 程序向标准输出打印结果:
第一行:所有合约的总成交额,即
第二行:所有合约的 VWAP (成交量加权平均价),计算方式为 Total Turnover / Total Volume。
样例输入
50000000 123plaintext样例输出
769984094225.13
174.9963plaintext运行环境、编译与测试#
本题将在 Huawei Kunpeng 920A (ARM64, 64-Core) 的 16 个核心上运行。评测容器镜像:cr.hpc.lcpu.dev/hpcgame/3rd-kunpeng920:latest,基础发行版为Fedora 43,安装了clang 21,flang 21和gcc 15。
可以使用以下命令编译程序:
g++ main.cpp -o main -O3 -fopenmp -std=c++17 -march=nativeplaintext我们会使用如下命令运行程序:
export OMP_NUM_THREADS=16
export OMP_PLACES=cores
export OMP_PROC_BIND=close
./main < input.txt > output.txtplaintext评分标准#
- 正确性:输出结果必须与标准答案的相对误差不超过1e-12。
- 性能:程序运行时间为 t,满分时间为t_0则得分为 min(20, 20^(t_0/t))。 共5个测试点,迭代次数有所不同。满分标准:
| 迭代次数 | 满分时间 | 最长运行时间 | 分值 |
|---|---|---|---|
| 5e7 | 0.35s | 0.7s | 20 |
| 5e8 | 3.5s | 7.0s | 20 |
| 1e9 | 7.0s | 14.0s | 20 |
| 5e9 | 35.0s | 70.0s | 20 |
| 5e10 | 349.0s | 698.0s | 20 |
Hint#
鲲鹏 CPU 有自己的硬件特质,了解这些特质有助于优化性能。
可以访问hpcgames ↗查找相关附件下载。
首先,我拿到这道题的时候,因为只需要修改结构体的定义,所以我先看了下 market.h 里面 Candle 结构体的定义:
struct Candle {
double high;
double low;
double close;
long long vol;
};c这个结构体一共包含了 3 个 double 类型的变量和 1 个 long long 类型的变量。根据 C++ 的内存对齐规则,double 类型通常需要 8 字节对齐,而 long long 也需要 8 字节对齐,看起来 这个结构体的内存布局应该是比较紧凑的,没有明显的内存浪费。
但是显然这道题不能不改就交上去(x),因此:我们需要考虑这个结构体在内存下的访问模式。
首先,我们的评测环境是 Kunpeng 920A,基于 ARMv8 架构的CPU, 其L1 L2 Cache Line Size 为 64 Bytes, L3 Cache Line Size 为 128 Bytes, 因此如果他们的粒度均为32 Bytes, 那多个Candle 对象可能会落在同一个 Cache Line 上, 造成不同核对同一 Cache Line 的争用,从而影响性能。
因此,一个很朴素的想法就是,增大结构体的大小,让每个 Candle 对象独占一个 Cache Line,从而避免不同核对同一 Cache Line 的争用,并且将结构体的起点对齐到 Cache Line 边界。
因此选择使用alignas(128)来对齐结构体,编译器会自动在结构体后面填充一些字节,使得每个 Candle 对象的大小为 128 Bytes。
struct alignas(128) Candle {
double high;
double low;
double close;
long long vol;
};c