Chapter 23: SIMD and GPU
⚠ Planned — not in Loup 1.25. This chapter describes the design in the Language Reference (§1-2.6 and §1-16.8) and the Packages Reference (§21.29, §21.30). The toolchain does not implement it yet, so its listings are design sketches and are not verified.
The specification gives native programs two routes to data-parallel speed: explicit CPU vector types in native.simd, and GPU compute kernels written in a shading language inside a template fence and dispatched through native.gpu. Neither ships in 1.25.1. native.simd, native.gpu and native.webgpu are not standard-library modules ('native.simd' is not a standard-library module), Vec[T, N] is an undefined name, and a wgsl fence parses as an ordinary template fence that nothing can compile or dispatch.
What a native program can use today is the LLVM backend's own optimizer: a --release build compiles through clang -O2 with ThinLTO (chapter 24), which auto-vectorizes simple loops over lists of numbers without any annotation.
CPU SIMD: native.simd
The planned module centres on one generic value type, Vec[T, N]: N lanes of a fixed-width number type, mapped directly to a hardware vector register. sizeof(T) × N must be 16, 32 or 64 bytes (an SSE/NEON, AVX or AVX-512 register); other widths are compile errors. Vectors are immutable values. They are built with splat(x) (every lane x), of(...) or load(list, offset), written back with store(list, offset), combined lane-wise with + - * /, and reduced or transformed with functions such as dot, sqrt, min, max and fma. A dot product:
@mode(host.native)
from native.simd import (Vec, dot);
func dotProduct(a: list[float32], b: list[float32]): float32 {
var sum: float32 = 0.0f32;
var i = 0;
while (i + 8 <= a.length()) {
let va = Vec[float32, 8].load(a, i);
let vb = Vec[float32, 8].load(b, i);
sum = sum + dot(va, vb);
i = i + 8;
}
while (i < a.length()) {
sum = sum + a[i] * b[i];
i = i + 1;
}
return sum;
}
Listing 23-1: A dot product with eight-lane vectors (design sketch)
The tail loop handles the elements that do not fill a whole vector. The @vectorize annotation is specified as a hint asking the compiler to vectorize a loop it would otherwise leave scalar.
SIMD is a host.native and freestanding feature. Using native.simd, Vec[T, N] or @vectorize in a host.dual file is specified as E_NATIVE_ONLY_IN_DUAL, because a host.dual program must also run as WebAssembly (§1-2.6).
GPU compute: shader fences
Three template-fence languages hold GPU shader source: wgsl (WebGPU), metal (Apple platforms only) and glsl (§1-16.8). A shader fence evaluates to the shader text; {$ … $} interpolations may only insert compile-time constants (int, float32, bool), and a runtime value is E_GPU_FENCE_RUNTIME. native.gpu compiles the text into a kernel and runs it over buffers (§21.30):
@mode(host.native)
from native.gpu import (GpuKernel, GpuBuffer, dispatch);
const WGSIZE: int = 256;
async func addOnGpu(a: list[float32], b: list[float32]): Task[list[float32]] {
let kernel = GpuKernel.compile(```(wgsl):(none)
@group(0) @binding(0) var<storage, read_write> out: array<f32>;
@group(0) @binding(1) var<storage, read> x: array<f32>;
@group(0) @binding(2) var<storage, read> y: array<f32>;
@compute @workgroup_size({$ WGSIZE $}, 1, 1)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i < arrayLength(&out)) {
out[i] = x[i] + y[i];
}
}
```);
let outBuf = GpuBuffer[float32].alloc(a.length());
let xBuf = GpuBuffer[float32].alloc(a.length());
let yBuf = GpuBuffer[float32].alloc(b.length());
await xBuf.copyFromHost(a);
await yBuf.copyFromHost(b);
let groups = (a.length() + WGSIZE - 1) / WGSIZE;
await dispatch(kernel, [groups], [outBuf, xBuf, yBuf]);
return await outBuf.copyToHost();
}
Listing 23-2: Adding two vectors on the GPU (design sketch)
Buffers are bound in order (binding(0), binding(1), …). Copies and dispatch are async because the GPU runs independently of the CPU, so GPU code also waits on the native async executor (chapter 21). The backend is chosen at build time (--gpu-backend wgpu|cuda|metal|vulkan, default wgpu) or at run time. The same wgsl text can also feed a render pipeline through native.webgpu (§21.29), which is the layer meant to be shared with browser code.
Structs shared with a shader need an agreed layout. @repr(C) fixes a Loup struct's layout (chapter 19); the shader's struct must then follow WGSL's own alignment rules, which pad a vec3<f32> to 16 bytes, so the Loup side adds explicit padding fields.
When the design lands
The usual guidance applies. GPU compute pays off for thousands to millions of independent, arithmetic-heavy elements whose data stays on the GPU between steps; CPU SIMD wins for smaller inputs, branchy code, and work that has to return to the CPU often. Before either, write the straightforward loop and measure a --release build, since the optimizer may already vectorize it.
Summary
native.simd(Vec[T, N], lane-wise operators,dot/sqrt/fma,@vectorize) is specified forhost.nativeandfreestandingand is ⚠ planned.- GPU kernels are specified as
wgsl,metalorglslfences compiled bynative.gpu.GpuKernel.compileand run withdispatch;native.gpuandnative.webgpuare ⚠ planned, and depend on nativeasync. - Today, rely on the LLVM backend's auto-vectorization in
--releasebuilds. - Chapter 24 covers building and distributing native programs.