flag
mode_edit

感觉七天时间还是太长了,好累

2.B 乘一乘

用 omp 并行矩阵乘法。模仿 cutlass 的写法,对 C 矩阵进行切分,每个线程算一块。

2.C 解方程

这个“五点格式求解 Poisson 方程”感觉比较神秘,大概看了下,是在求解线性方程组。后来搜索到了 https://zhuanlan.zhihu.com/p/130808947 。发现原来是直接对着式子算就行了。

2.D 道生一

主要在优化排序部分。首先对原数组进行切分,对每部分分别使用 vqsort, 然后用手写的 merge 进行合并。

这里的并行 merge 有一些 trick. 可以用两个线程,一个线程正向 merge, 另一个反向 merge, 最终在中间相遇,可以提高并行度。此外,还有 merge path 等并行度更高的方法,由于懒,所以没写。。最后是 160⁄200 左右。

另外似乎还可以用 radix sort 之类的东西,这个确实没有研究过。

2.E 卷积

手写一下 AVX512。由于卷积核比较大,且 n = channel = 1. 因此实际上是在算 stencil。写法大概是把整个卷积核都放进 mm512 里面去。然后在图像上纵向滑动。每移动一行,就在向量寄存器中替换一行。这样做对 cache 和访存比较友好

不过最后发现,好像 IO 的时间比计算卷积的时间多得多…

template <int m, int n>
void simd_version(Mat &input, KernelMat<m, n> &kernel, Mat &res) {
  constexpr int vec_width = 512 / 8 / sizeof(float);
  __m512 kernel_vec[m][n / vec_width];
#pragma unroll(2)
  for (int i = 0; i < m; ++i) {
    for (int j = 0; j < n / vec_width; ++j) {
      kernel_vec[i][j] = _mm512_load_ps(&kernel.get(i, j * vec_width));
    }
  }
  for (int qi = 0; qi < res.n; ++qi) {
    __m512 input_vec[m][n / vec_width];
#pragma unroll(2)
    for (int i = 0; i < m; ++i) {
      for (int j = 0; j < n / vec_width; ++j) {
        input_vec[i][j] = _mm512_loadu_ps(&input.get(i, qi + j * vec_width));
      }
    }
    for (int pi = 0; pi < res.m; ++pi) {
      __m512 res_vec = {0};
      int offset = pi % m;
#pragma unroll(2)
      for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n / vec_width; ++j) {
          res_vec = _mm512_fmadd_ps(kernel_vec[i][j],
                                    input_vec[(i + offset) % m][j], res_vec);
        }
      }
      res.get(pi, qi) = _mm512_reduce_add_ps(res_vec);
      if (pi + 1 < res.m) {
        for (int j = 0; j < n / vec_width; ++j) {
          input_vec[offset][j] =
              _mm512_loadu_ps(&input.get(pi + m, qi + j * vec_width));
        }
      }
    }
  }
}

3.A 造 AI

这个 cifar10 是真的难搞。从网上抄了一个 resnet18, 然后开始嗯炼。发现想搞到 90+ 以上还是很困难。于是搞了一大堆 trick:

  • 数据增强:把图片随机裁切,缩放
  • 学习率调整:用了个神秘的 scheduler
  • 梯度 clip:这个不知道是干啥的…

中间还遇到了一些乌龙…比如训练集上 normalize 了,而测试集没有,于是正确率越来越低。反倒是 section2 简单了很多,随便就到 90+了。

3.B 焦散

感觉这个有意思。题意大概是,在 z=0 平面上的单位正方形上,向上投射出很多垂直与平面的光线。光线依次通过若干个和单位正方形一样大的玻璃板。每个玻璃板上有若干透镜,试求每一条光线最终打到的位置的坐标。

原始的代码逻辑是:对于每一个采样点(也就是每一条光线),遍历每一块玻璃板,遍历所有透镜,看看有没有相交的。如果存在相交,那么据此计算该透镜对光线偏折方向的贡献。将一块玻璃板所对应的贡献累加起来,就能求出光线经过玻璃板后的传播方向。

原始代码可以很轻松地改写成 GPU 代码:有几个采样点就开几个线程就可以了。但是这还可以进一步优化。通过注释部分代码。可以发现遍历所有透镜并挨个求交,是最热的部分。考虑到覆盖到一个点的透镜数量其实应该不多,因此大部分计算都浪费掉了。

我本来写了个 bvh,来试图加速这一部分,没想到开销有点大,变成了负优化。后来改成了网格预处理的方式。把玻璃板切分成网格,提前预处理每一个透镜都和哪些格子相交了,之后就可以根据点在网格中的位置,只遍历对应网格的透镜,从而减少求交的计算量。例如,如果切成了 3*3 的网格,最理想的情况下,就只需要 1⁄9 的计算量。

除此之外,还需要注意 block 的划分和任务的分配,应该让同一个 warp 的线程所处理的采样点,在空间上比较接近。我这里好像写的比较垃圾。。于是得了 180⁄200 分。

4.A 扫雷

大概是模拟了我本人玩扫雷的策略。

  • 随机点开一片区域
  • 对于边界上的点,挨个双击一下,看看有没有发现
  • 如果边界上的点用完了,就随机再开一片区域

该策略在小地图上可以拿到 90% 的分数。对于更大的地图,可以切分为多个区域,分别由不同线程处理即可。

4.B AI 算子优化

说实话这题没整明白,暴力交上去得了 80.调换了一下 gemm 的 mnk 顺序,于是变成了 101⁄150 分。101 分还卷什么,我直接开躺。

总结

我个人觉得比较有意思的是焦散这个题,确实比较大脑升级。另外没想到排序的科技居然这么多..真的是出乎意料。剩下的 fft 和 rdma 两个题确实是知识盲区..真不会

navigate_before navigate_next