Phase 2, week 10retrospective

Hand-written RVV intrinsics matmul

A hand-written RVV matmul sets your own schedule's ceiling, not the machine's; on QEMU it is a work proxy, not a hardware limit.


The kernel

#include <riscv_vector.h>

void matmul(const float *A, const float *B, float *C, int M, int N, int K) {
  for (int i = 0; i < M; i += MR) {
    for (int j = 0; j < N; ) {
      size_t vl = __riscv_vsetvl_e32m1(N - j);
      vfloat32m1_t acc[MR];
      for (int m = 0; m < MR; ++m)
        acc[m] = __riscv_vfmv_v_f_f32m1(0.0f, vl);
      for (int k = 0; k < K; ++k) {
        vfloat32m1_t b = __riscv_vle32_v_f32m1(B + k * N + j, vl);
        for (int m = 0; m < MR; ++m)
          acc[m] = __riscv_vfmacc_vf_f32m1(acc[m], A[(i + m) * K + k], b, vl);
      }
      for (int m = 0; m < MR; ++m)
        __riscv_vse32_v_f32m1(C + (i + m) * N + j, acc[m], vl);
      j += vl;
    }
  }
}

Build it with clang --target=riscv64-linux-gnu -march=rv64gcv -mabi=lp64d -O3 -mrvv-vector-bits=zvl. Names and flags are release-bound: confirm with clang --target=riscv64-linux-gnu --help | Select-String rvv. The row loop assumes MR divides M; that guard is the tail work in p2-07.

What it measures

This kernel is the practical ceiling for the project, and the ceiling is your own best schedule rather than the machine’s peak. That distinction belongs in the results file, because a reader who sees “hand-written” will assume hardware limit. -mrvv-vector-bits=zvl is part of the same statement: it fixes VLEN at the build’s minimum so the compiler can unroll harder, and it gives up a binary that adapts to a wider machine.

The accumulation order is ascending K, the same as the scalar reference, so the only numeric difference is fusion. If the reference was built with contraction off, vfmacc rounds once where mul plus add rounds twice, and a bit-exact check fails on some shapes. Pin the reference’s -ffp-contract setting or state the ulp tolerance in the harness.

Things that break

The vsetvl suffix and the intrinsic’s LMUL disagree, for example __riscv_vsetvl_e32m1 returning a length that is then used with an m2 intrinsic. It compiles, because each intrinsic is its own function, and the compiler repairs vtype with an extra vsetvli. You see it between two vfmacc instructions. The other failure is a silently transposed A: A[(i + m) * K + k] assumes row-major, and a column-major caller gives plausible wrong numbers that only the reference check catches.

Sources

A version records the documentation the claim was checked against, not a house pin. Confirm flags with --help on the release you installed.

Check yourself

What does the hand-written kernel actually bound?

Why can `vfmacc` differ from the scalar reference at the last bit?

What does `-mrvv-vector-bits=zvl` trade away?

0 / 3 correct

← back to the map