Skip to content

[RVV] add rvv f32 kernels for CHW ibilinear#10771

Draft
leduclean wants to merge 2 commits into
google:masterfrom
leduclean:pr2-rvv-ibilinear-chw
Draft

[RVV] add rvv f32 kernels for CHW ibilinear#10771
leduclean wants to merge 2 commits into
google:masterfrom
leduclean:pr2-rvv-ibilinear-chw

Conversation

@leduclean

@leduclean leduclean commented Jul 17, 2026

Copy link
Copy Markdown

CHW counterpart of the f32-ibilinear RVV kernels added in #10682.

Stacked on #10770 (the benchmark these numbers come from). The first commit of this PR belongs to that one; review only Add RVV f32 microkernels for CHW ibilinear.

Tested on K230 (C908, VLEN=128, in-order, single core, ~1.6 GHz), median of 3 repetitions, CV below 0.7% on every row. Scalar is scalar_p4, which is what ibilinear-chw-config.c selects on RISC-V today and what this PR replaces.

Workload Scalar (ns) RVV 2x2v (ns) Speedup
12x8 -> 24x16, C=256 1,411,296 1,334,479 1.06x
5x33 -> 10x33, C=192 828,064 858,850 0.96x
20x33 -> 40x33, C=96 1,909,567 1,738,336 1.10x
6x5 -> 12x10, C=96 131,713 136,368 0.97x
24x20 -> 48x40, C=48 1,357,597 1,200,751 1.13x
80x66 -> 160x66, C=24 4,036,698 3,264,116 1.24x
256x256 -> 512x512, C=3 12,923,172 13,815,761 0.94x
256x256 -> 512x512, C=4 17,200,065 17,747,953 0.97x
64x64 -> 128x128, C=128 34,060,238 35,662,172 0.96x
32x32 -> 128x128, C=256 67,248,289 71,139,741 0.95x
128x128 -> 256x256, C=32 34,355,363 36,021,590 0.95x

Geomean 1.071x on the six decoder shapes, 0.952x on the five large ones, 1.015x overall. The decoder shapes are the ones the CHW path actually serves (sparse inference, segmentation decoders); the large shapes are deliberate stress cases and are reported as regressions rather than omitted.

Why the gain is modest, unlike #10682's 4-6x. NHWC has a contiguous dimension: f32-ibilinear-rvv-u2v vectorizes along channels with plain unit-stride vle32, no gather at all. CHW is transposed the four taps of a pixel sit at arbitrary addresses so the kernel must gather.
On this core vluxseg2ei64 serializes its element accesses: rvv_2x2v retires 6.3x fewer instructions than scalar_p4, but each costs 6.7x more cycles (CPI ~9 against ~1.4). The kernel is bound by the gather, not the ALU.

Two alternatives were measured and dropped: staging the taps through scalar loads NEON-style (0.75-0.82x RVV has no lane-insert from memory, so the stack round trip costs more than the gather saves), and vectorizing across channels with strided loads (0.05x on any power-of-two spatial size, from L1 set aliasing when input_increment is a multiple of the way size).

All numbers are from one core. They should not be read as a statement about RVV in general a core with a parallel gather could reorder the whole table.

There was no benchmark for this microkernel family, so changes to the CHW
bilinear kernels could not be measured.

Build the indirection buffer and the interpolation coefficients with
xnn_indirection_init_resize_bilinear2d_chw_f32 -- the operator's own code, as
bench/f32-dwconv.cc already does for dwconv -- so the benchmark cannot drift
from the layout the operator actually produces. Two properties of that layout
dominate the memory behaviour and are easy to get wrong by hand: the two
indirection pointers of a pixel are one input row apart, and channel planes
are disjoint, with input_increment == input_height * input_width *
sizeof(float).

Parameterize by {InW, InH, OutW, OutH, C} and reuse the decoder shapes of
bench/operators/resize-bilinear-nhwc.cc, so the microkernel numbers stay
comparable with the operator benchmark, plus a few working sets larger than
cache.

Rotate buffers across iterations following the f32-dwconv idiom. The input
rotates via input_offset, which is the operator's own rebasing mechanism.

Register every f32-ibilinear-chw ukernel: scalar p1/p2/p4, neon and neonfma
p4/p8/p16, sse p4/p8, and wasmsimd p4/p8.
CHW counterpart of the f32-ibilinear RVV kernels added in google#10682.

Up to 1.24x over the scalar_p4 this replaces, 1.1x geomean on the decoder
shapes that motivated the kernel. Measured on K230 (C908, VLEN=128, in-order,
single core, ~1.6 GHz) with bench/f32-ibilinear-chw:

  decoder shapes (6):  geomean 1.1x   (0.98x .. 1.24x)
  large shapes   (5):  geomean 0.96x  (0.95x .. 0.98x)
  all shapes    (11):  geomean 1.03x

The four taps are gathered with vluxseg2ei64, indexed off the indirection
pointers. The kernels are generated from rvv.c.in over two parameters:
CHANNEL_TILE, which amortizes the indirection and weight re-reads across a block and
LMUL.

Select rvv_2x2v when xnn_arch_riscv_vector is present, and keep scalar_p4
otherwise. rvv_2x2v dominates the other three variants on all 11 shapes, which
is why it is the one selected; the others are kept as the family that makes
that choice reproducible on other cores, as with neon p4/p8/p16.

The gain is bounded by the gather, not the ALU: rvv_2x2v retires 6.3x fewer
instructions than scalar_p4, but each costs 6.7x more cycles (CPI ~9 against
~1.4), because vluxseg2ei64 serializes its element accesses on this core. A
core with a better indexed-segment-load path should see more of the 6.3x.

Signed-off-by: Le Duc Léandre <lleduc@kalrayinc.com>
@google-cla

google-cla Bot commented Jul 17, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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