On Thu, 13 Aug 2026 22:48:49 GMT, Chad Rakoczy <[email protected]> wrote:
> Adds support for vectorized dot product on aarch64 (`sdot` and `udot`) > through the Vector API > > Dedicated dot product instructions have been shown to provide up to 10x > throughput for lucene > ([results](https://github.com/apache/lucene/pull/13572)) compared to > vectorized multiply and add. This PR adds two new methods to `ByteVector` to > leverage the aarch dot product instructions `sdot` and `udot` respectively. > - `IntVector dot(Vector<Byte> v, Vector<Integer> acc)` > - `IntVector dotUnsigned(Vector<Byte> v, Vector<Integer> acc)` > > Each int lane of the result holds the dot product of the corresponding group > of four bytes from the two operands, added to the matching accumulator lane. > For example: > > a = [a1, a2, a3, a4, ..., ..., a13, a14, a15, a16] > b = [b1, b2, b3, b4, ..., ..., b13, b14, b15, b16] > acc = [acc1, ..., ..., acc4] > > a.dot(b, acc) -> [ > acc1 + a1 * b1 + a2 * b2 + a3 * b3 + a4 * b4, > ..., > ..., > acc4 + a13 * b13 + a14 * b14 + a15 * b15 + a16 * b16 > ] > > > The equivalent instructions on x86 are `VPDPBSSD` and `VPDPBUUD` which > perform the same operations and match the proposed new functions however this > PR only includes aarch64. > > Graviton 2 > > Benchmark Mode Cnt Score Error Units > VectorDotBenchmark.dotScalar thrpt 25 2412.719 ± 0.014 ops/ms > VectorDotBenchmark.dotMulAdd thrpt 25 1497.953 ± 2.734 ops/ms > VectorDotBenchmark.dotVector thrpt 25 23243.016 ± 141.708 ops/ms > VectorDotBenchmark.dotUnsignedScalar thrpt 25 2402.966 ± 0.157 ops/ms > VectorDotBenchmark.dotUnsignedMulAdd thrpt 25 1498.898 ± 1.442 ops/ms > VectorDotBenchmark.dotUnsignedVector thrpt 25 23344.116 ± 222.150 ops/ms > > > Graviton 3 > > Benchmark Mode Cnt Score Error Units > VectorDotBenchmark.dotScalar thrpt 25 7946.950 ± 2.564 ops/ms > VectorDotBenchmark.dotMulAdd thrpt 25 3257.311 ± 5.308 ops/ms > VectorDotBenchmark.dotVector thrpt 25 41430.996 ± 688.936 ops/ms > VectorDotBenchmark.dotUnsignedScalar thrpt 25 2536.268 ± 0.438 ops/ms > VectorDotBenchmark.dotUnsignedMulAdd thrpt 25 3259.853 ± 3.511 ops/ms > VectorDotBenchmark.dotUnsignedVector thrpt 25 41263.821 ± 478.856 ops/ms > > > --------- > - [x] I confirm that I make this contribution in accordance with the [OpenJDK > Interim AI Policy](https://openjdk.org/legal/ai). Hi, I'd like to add a +1 for this with a concrete use case. I also think having x86 support would be really cool. I have been building for some time a pure Java media library that uses the Vector API for its codecs: AV1, HEVC, H.264, VP9 and VP8 decoding and encoding, WebP, AAC and so on. I've been benchmarking every codec throughout development against FFmpeg and dav1d. I have plans to release this work soon, but I was trying to reach performance parity in as many workloads as possible against these implementations before doing so. Most of the remaining gap comes from one thing: the Vector API has no integer multiply-add for narrow types. Right now every byte-times-coefficient kernel widens the bytes, multiplies in wider lanes and adds, which costs 2 to 4 times the instructions of the native code. What I think I would need is dot products over 8-bit pixels or 16-bit coefficients, so this PR is exactly the primitive I'm missing. On the points raised above: - I don't think dot product is a snowflake op, at least not in media code. Basically every motion compensation filter, transform and SATD/RD cost kernel in FFmpeg, dav1d and libvpx is built on `pmaddubsw`/`pmaddwd` on x86 or `sdot`/`udot`/`usdot` on Arm. x86 has had the 16-bit pair version (`pmaddwd`) since SSE2, so it's about as portable as integer SIMD gets. - The scalar-returning `int dot(Vector<Byte>)` wouldn't work for my use case. A filter needs one group sum per output sample: each output pixel is the sum of 4 or 8 taps times 4 or 8 pixels. So the current shape, with a group-wise `IntVector` result and an accumulator, is exactly what I need. Reducing to a scalar on every call would make it unusable for filters and transforms. - I'd also be careful with recognizing the pattern in C2 instead. I already spend a lot of time writing kernels in the specific shapes C2 handles well; for example, range checks only get hoisted when the index has a constant scale, so I keep one kernel per block size. If the dot product only kicks in when I hit a matched IR shape, that's the same brittleness as auto-vectorization, which is the reason I'm using the Vector API in the first place. An explicit op with an exact fallback (widen, mul, add) is predictable. A few things: 1. I think x86 support could be added. With AVX-VNNI / AVX512-VNNI this maps to `vpdpbusd` (and `vpdpbssd` / `vpdpbuud` on newer cores). On plain AVX2 a fallback through zero-extend + `vpmaddwd` would still beat what we can write today. It has to keep exact semantics, so not `pmaddubsw`, since that saturates. 2. I'm not sure whether this is covered, but mixed signedness (unsigned x signed) would be very important for some use cases, like video. Pixels are unsigned bytes and filter taps are signed, which is exactly what `vpdpbusd`, `pmaddubsw` and aarch64's `usdot` do. With only signed x signed and unsigned x unsigned we'd still have to widen or bias the pixels first. 3. I think a 16-bit variant (e.g. `ShortVector.dot` into `IntVector`, pairs of shorts per int lane) would also be very useful: this is `pmaddwd` on x86 (`vpdpwssd` with VNNI), and it's what transforms and the second filter pass of motion compensation need. Maybe framing these as one family, a widening dot over groups of 4 bytes or 2 shorts into int lanes, would also make it feel less like a one-off operation. I'm happy to test any of this on my codecs (x86 AVX2 and AVX-512 machines) and share before/after numbers if that helps the review. ------------- PR Comment: https://git.openjdk.org/jdk/pull/32359#issuecomment-6135439361
