arm-sve · git:20260905.fd53e99 · 2026-09-05 · sha256 7d0000cc941b2ffe

arm-sve git:20260905.fd53e99A

Immutable. This exact content is served forever at /api/v1/blob/7d0000cc941b2ffe.

---
name: arm-sve
description: 'Use when writing or porting AArch64 SIMD to SVE or SVE2: arm_sve.h intrinsics, predicates, vector-length-agnostic loops, auto-vectorization, or SVE registers in GDB. Not for NEON: use simd-intrinsics.'
---

# Arm SVE and SVE2

## Contract

| Field | Bound contract |
|---|---|
| Trigger | AArch64 code needs vector-length-agnostic SIMD: a NEON loop is being ported, an `arm_sve.h` kernel is being written, auto-vectorization to SVE is being checked, or SVE register state is being inspected in GDB. |
| Authority | Reversible local: writes only the source files the user names; rollback is reverting them in version control. No remote mutation. |
| Side effect | New or edited C sources. Runs the binary on the host or under QEMU for verification. |
| Done | The kernel compiles with `-march=armv9-a+sve2`, produces the same result as the scalar reference for lengths that are not a multiple of the vector length, and runs correctly at two different vector lengths (hardware and QEMU, or two QEMU `sve<N>` settings). |

## Inputs

- Source loop or NEON kernel to port, with a scalar reference to compare against.
- Target machines and their SVE level: Graviton3 (Neoverse V1) has SVE, Graviton4 (Neoverse V2) has SVE2, Apple M1 through M3 have NEON only, Apple M4 has SME with streaming SVE and no non-streaming SVE.
- Compiler: GCC or Clang, and version (`gcc --version`, `clang --version`).
- GDB version if debugging (`gdb --version`).

## Procedure

1. Confirm the hardware and the compiler agree. Vector length (VL) is 128 to 2048 bits in 128-bit steps and is fixed per CPU; the code must not assume it. Done when: `/proc/cpuinfo` lists `sve` (and `sve2` if needed) and the compiler defines `__ARM_FEATURE_SVE` under the chosen `-march`.

   ```bash
   grep -o -m1 -E 'sve2?' /proc/cpuinfo
   clang --target=aarch64-linux-gnu -march=armv9-a+sve2 -dM -E - </dev/null | grep __ARM_FEATURE_SVE
   ```

2. Write the kernel with predicates instead of a scalar tail. `svwhilelt_b32(i, n)` is true for lanes with `i + lane < n`, so the last iteration is partial with no cleanup loop. `svcntw()` returns the 32-bit lanes per vector at runtime. Done when: the loop below matches the scalar `y[i] += alpha * x[i]` for `n = 0`, `n = 1`, `n = svcntw() - 1`, and `n = 3 * svcntw() + 1`.

   ```c
   #include <arm_sve.h>
   #include <stddef.h>

   void saxpy_sve(float *y, const float *x, float alpha, size_t n) {
       svbool_t pg = svwhilelt_b32((uint64_t)0, (uint64_t)n);
       size_t i = 0;
       do {
           svfloat32_t vx = svld1_f32(pg, &x[i]);
           svfloat32_t vy = svld1_f32(pg, &y[i]);
           vy = svmla_n_f32_x(pg, vy, vx, alpha);
           svst1_f32(pg, &y[i], vy);
           i += svcntw();
           pg = svwhilelt_b32((uint64_t)i, (uint64_t)n);
       } while (svptest_any(svptrue_b32(), pg));
   }
   ```

   ```bash
   gcc -march=armv9-a+sve2 -O3 -o saxpy saxpy.c
   ```

   Registers: `Z0` to `Z31` hold data, `P0` to `P15` hold predicates. `svcntb()` gives bytes per vector; `svcntw()` and `svcntd()` give 32-bit and 64-bit lane counts.

3. Pick the predication suffix on purpose. `_m` merges: inactive lanes keep the first operand's value. `_z` zeroes inactive lanes. `_x` leaves inactive lanes unspecified and lets the compiler pick the cheapest encoding. Loads and stores are always predicated, so a result only leaks through inactive lanes when an `_x` value is stored unpredicated or reduced. Done when: every intrinsic in the kernel has the suffix its consumer needs, and reductions over a partial vector use `_z` or a predicated reduction.

   | Intrinsic | Purpose |
   |---|---|
   | `svld1_f32(pg, ptr)` | Predicated load |
   | `svst1_f32(pg, ptr, v)` | Predicated store |
   | `svmul_f32_x(pg, a, b)` | Multiply |
   | `svmla_f32_x(pg, acc, a, b)` | Fused multiply-add |
   | `svwhilelt_b32(i, n)` | Predicate for the lanes still inside `n` |
   | `svcntw()` | 32-bit lanes per vector |
   | `svptrue_b32()` | All lanes active |
   | `svdot_s32(acc, a, b)` | Signed 8-bit dot product into 32-bit lanes; unpredicated, in base SVE |

4. Check auto-vectorization before hand-writing intrinsics. Both compilers vectorize simple loops to SVE at `-O3`; the remarks say so. Done when: the remark for the hot loop reports a `vscale` width, or the reason it failed is known.

   ```bash
   gcc   -march=armv9-a+sve2 -O3 -fopt-info-vec -c app.c
   clang -march=armv9-a+sve2 -O3 -Rpass=loop-vectorize -c app.c
   # clang prints: remark: vectorized loop (vectorization width: vscale x 4, ...)
   ```

   `#pragma omp simd` (with `-fopenmp-simd`) tells the compiler a loop has no loop-carried dependence when it cannot prove it.

5. Port a NEON loop. Replace the fixed stride (`i += 4`) with `svcntw()`, replace the scalar tail with a `svwhilelt` predicate, and pick `_m`, `_z`, or `_x` per step 3. Done when: the result matches the NEON version bit for bit for the same inputs, at every tested `n`.

6. Test at more than one vector length. QEMU's `max` CPU enables every SVE length by default; naming `sve<N>=on` properties enables only those lengths, and without KVM every smaller power-of-two length must be enabled too. The guest runs at the largest enabled length. Done when: the binary passes at VL 128 and at a larger VL.

   ```bash
   qemu-aarch64 -cpu max,sve128=on ./saxpy_test
   qemu-aarch64 -cpu max,sve128=on,sve256=on ./saxpy_test
   qemu-aarch64 -cpu max ./saxpy_test         # largest length QEMU supports
   ```

7. Debug SVE state in GDB. GDB has supported SVE on AArch64 Linux since version 8.2; the installed 17.2 does, and register width follows the VL of the running process. Done when: `p $z0` prints a union view of the vector and `p $p0` prints the predicate.

   ```
   (gdb) break saxpy_sve
   (gdb) run
   (gdb) p $z0
   (gdb) p $p0
   (gdb) info registers z0 z1 p0
   ```

8. Guard the build for non-SVE machines. Wrap the SVE path in `#if defined(__ARM_FEATURE_SVE)` with a NEON or scalar fallback, or dispatch at runtime on the `sve` feature bit. Done when: the same source builds and runs on an M1 and on a Graviton3.

## Failure and recovery

| Symptom | Cause | Fix |
|---|---|---|
| `SIGILL` on first SVE instruction | CPU without SVE (Apple M1 to M3), or M4 outside streaming mode | Check `/proc/cpuinfo` or `sysctl hw.optional.arm`; fall back to NEON. |
| Wrong values in the last vector | Inactive lanes from an `_x` operation were stored or reduced unpredicated | Use `_z` for the value feeding a reduction; keep stores predicated. |
| Slower than the NEON version | Very short arrays where the predicate setup dominates, or an unaligned store pattern | Measure with the array sizes the program uses; keep NEON or scalar for `n` below one vector when the measurement says so. |
| No vectorization remark | Unknown trip count with a possible alias, or floating-point reduction without `-ffast-math` | Add `restrict`, `#pragma omp simd`, or `-fno-trapping-math` after confirming the semantics allow it. |
| Build fails with SVE intrinsics on M3 | `-march` lacks `+sve` or the target has none | Guard with `__ARM_FEATURE_SVE`. |
| GDB prints no `z` registers | GDB older than 8.2, or the process is not on SVE hardware | Update GDB; run on SVE hardware or under QEMU with gdbstub. |

## Output

SVE source in the named files that matches the scalar reference at partial lengths and at two vector lengths, plus a note with the compiler remark for the hot loop and the machines the kernel was verified on.