Skip to content

riscv64: 3 of 4: 8 and i32 RVV matmul kernels - #2601

Open
czoli1976 wants to merge 4 commits into
sonos:mainfrom
czoli1976:riscv64-rvv-i32
Open

riscv64: 3 of 4: 8 and i32 RVV matmul kernels#2601
czoli1976 wants to merge 4 commits into
sonos:mainfrom
czoli1976:riscv64-rvv-i32

Conversation

@czoli1976

@czoli1976 czoli1976 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2599 and #2600. GitHub cannot base a pull request on a fork branch,
so this targets main and its diff carries those PRs' commits too. The only new
commit here is 0b3bce0f — the rest are reviewed in #2599 and #2600.

Stacked on the f32 and f16 tiers. Adds four i32-accumulator kernels — 8x8 and
16x8 GEMM, 16x1 and 32x1 GEMV — covering both packings the frame offers:
i32 x i32, and i8 x i8 accumulating into i32.

The i8 inner loop

vtype sits at e16 for that loop, which buys two things at once. vle8.v takes
EEW=8 from the instruction rather than from vtype, so the packed A column loads
as bytes with no separate widening step beyond one vsext.vf2; and
vwmacc.vx widens on the way in, landing straight in the e32 accumulators.
Because VLMAX at e16/mN equals VLMAX at e32/m2N, a single vl of MR is valid
in both states, so the loop needs one vsetvli on entry and one on exit rather
than a pair per K step.

Zvqdotq would collapse this further, but it is unratified and no hardware
implements it yet.

Quantised ops

QScale, RoundingShiftRight and ShiftLeft follow the sign-magnitude
reference in generic/rounding.rs rather than an arithmetic shift, so the six
rounding policies differ only in the nudge term. Two things make that cheaper
than it looks:

  • The negative mask is taken before the magnitude overwrites the sign, and it
    serves the MinusInf and PlusInf nudges as well. PlusInf wants v <= 0 rather
    than v < 0, but those differ only at v == 0, where half + nudge never
    survives the shift and the result is 0 under every policy.
  • For the same reason signum needs no zero case.

QScale widens to e64 for the i64 product, rounds there, and truncates back,
matching the reference which computes in i64 and casts.

Testing

test_mmm_kernel_i32! pulls in mmm_q_scale_tests!, so every policy is
proptested against the reference — that is what these were developed against.
2683 tract-linalg tests and tract-core's 270 pass at VLEN 128, 256, 512 and
1024, on stock RVA23, and with V absent, under both qemu 10.2.1 and 11.0.3.

No CI change needed; the entries the f32 PR added cover this tier.

Same caveat as the earlier two: correctness under emulation, no hardware
numbers.

🍍

@czoli1976 czoli1976 changed the title riscv64: i8 and i32 RVV matmul kernels riscv64: 3 of 4: 8 and i32 RVV matmul kernels Aug 7, 2026
ckristian added 2 commits August 12, 2026 07:23
tract had no RISC-V backend, so rv64gc ran the generic Rust kernels for every
matmul. Add an RVV 1.0 f32 mmm tier, detecting V from the AT_HWCAP bit that
Linux never sets for the incompatible 0.7.1 draft and VLEN from the vlenb CSR.
Because VLEN is a runtime property while MR must be a const generic, each
kernel fixes (MR, NR, LMUL) and pins vl to MR, which is correct wherever
VLMAX >= MR and short below it, so dispatch is gated on the hart's VLMAX
reaching MR and the kernel re-checks the granted vl before running. An
assembler probe keeps toolchains predating RVV 1.0 on the generic fallback.
The RVV kernels are gated on the hart's vector length, so a single emulated
width would leave half the kernel set untested. Add riscv64gc to the qemu
cross-test platforms twice, at VLEN 256 and 128, which select disjoint halves.
-cpu max rather than a profile model because the generic rv64 model cannot run
Debian's riscv64 glibc at all.
ckristian added 2 commits August 12, 2026 10:41
The riscv64 tier covered f32 only, so f16 matmul fell back to the generic
kernels even on parts with native half-precision vectors, which includes the
SpacemiT X60 in the K1. Add an f16 mmm tier from the same template at SEW=16,
where VLMAX doubles and so does every tile height for a given LMUL and VLEN.
Zvfh is read from the /proc/cpuinfo isa line, since RVA23 mandates only
Zvfhmin and that cannot hold an f16 accumulator, and a second assembler probe
keeps toolchains predating Zvfh on the f32 tier alone.
The riscv64 tier had no integer kernels, so quantised models fell back to the
generic ones for every matmul. Add an i32 accumulator tier handling both
packings the frame offers: i32 x i32 at e32, and i8 x i8 through a loop at e16
where vle8.v picks EEW=8 off the instruction and vwmacc.vx widens straight into
the e32 accumulators, which keeps VLMAX identical between the two so one vl
serves both. QScale, RoundingShiftRight and ShiftLeft follow the sign-magnitude
reference in generic/rounding.rs, widening to e64 for the multiply; the
negative-value mask taken before the magnitude overwrites it also serves the
MinusInf and PlusInf nudges, which differ only where the result is zero anyway.
@czoli1976

Copy link
Copy Markdown
Contributor Author

Rebased onto current main; merge conflict resolved.

This was downstream of #2599, whose conflict was a duplicate of fb4bf2f (the hwbench fallback, since landed upstream). Dropping that one commit cleared this PR too — everything else cherry-picked onto current main with no conflicts. The only new commit here is now 0b3bce0f.

Re-verified on the new base under qemu-riscv64 11.0.3: the full stack passes 2709 tract-linalg tests at VLEN 128, 256 and 512, on stock rva23u64, and with V absent. cargo fmt --all clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant