Write one data-parallel kernel for several CPU vector widths. Get the promised machine code—or a compiler error.
Modern CPUs can apply one instruction to several numbers held together in a vector register, a model called SIMD. Rake calls one target-sized vector of values a rack, and each value position inside it a lane. A native Rake profile names a CPU target and the machine-code properties required for that target. Its central promise is that every live rack stays in one physical SIMD register. If the compiler's backend—the part that produces machine code—cannot preserve that representation, compilation stops with the failed obligation.
Compute kernels often store each field in its own contiguous column, a layout called a
structure of arrays. One rack is one target-sized chunk of one such
column: four f32 positions on 128-bit SSE2 or Arm NEON, eight on 256-bit
AVX2, or sixteen on 512-bit AVX-512. A stack declares the columns in the
dataset. A pack connects that declaration to the complete columns and
their element count. A for … using … up to … traversal visits the pack one
rack at a time.
The caller supplies two contiguous f32 columns, one dense u8
age column, and a runtime count.
The Particles stack below declares their columnar schema; the pack connects
that schema to the complete 400-particle storage. On each iteration, particle.position_x
and particle.velocity_x are racks containing the current target-width chunk.
The narrower age column requires an explicit widen before computation. The
source never names a CPU-specific vector function or lane count.
stack Particles {
f32: position_x, velocity_x;
u8: age;
}
run advance_x(
particles: pack Particles,
<count: i64>,
<dt: f32>
) -> f32:
for particle in particles using f32s up to <count>:
let ages: u32s = widen(particle.age)
yield particle.position_x + particle.velocity_x * <dt>
The notation exposes distinctions that affect the machine. pack Particles
reads as “a pack of Particles”; using f32s names the native compute domain;
and angle brackets mark count and dt as one uniform scalar shared
by every lane. Seeing many scalar markers in a kernel should provoke the question: could
these values vary by lane, or be stored and processed more efficiently?
Inside the stack, each stored type comes before all columns that share it. That makes
memory density readable down the left edge: the two four-byte f32 columns are
grouped separately from the one-byte u8 column. The plural spelling keeps the
relationship compact: f32 is one stored element; f32s is one
target-native rack of them.
Compiling the same source for a named profile chooses the rack width. Four hundred
f32 values divide exactly, so no tail is needed:
| Target profile | Float lanes per rack | 400-particle traversal |
|---|---|---|
| SSE2 / NEON | 4 | 100 racks |
| AVX2 | 8 | 50 racks |
| AVX-512 | 16 | 25 racks |
The programmer does not maintain three target-specific vector kernels, three loop strides, and three ways to handle a final partial chunk. Rake instead produces a separate executable for each selected target. The choice is made while compiling, not by a width check each time the kernel runs.
Rust made a compelling modern case for a strict compiler: rejecting plausible programs can be more useful than accepting them when the diagnostic explains which safety rule the program violated and how to repair it. Rake applies that bargain to declared performance properties. A compute kernel is often written because its machine shape matters, so losing vectorization is a failed requirement rather than an incidental missed optimization.
Rake does not turn every performance surprise into a language error. The programmer opts into a named native profile with specific obligations. If an irregular access requires a forbidden gather from non-contiguous addresses, an operation requires scalar helper calls, or register pressure would spill a live rack, the compiler identifies the source operation and the failed obligation before the object ships. The programmer can change the algorithm or choose a profile whose published contract permits the required operation.
Rust's ownership diagnostics →
Consider a rack-wide sine operation when the backend has no compliant packed sine sequence. Ordinary C and Rust compilation can accept the source and emit scalar library calls. Rake refuses to describe that result as native rack execution.
The program compiles. Under the strict comparison build, the resulting loop contains scalar sinf calls. A different toolchain or flag set may make a different choice.
The parser and type checker understand sin(values), but machine-code emission stops before assembly.
call to 'sin' is not supported by native crunch lowering
| Approach | What remains the programmer's responsibility |
|---|---|
| C / Rust scalar loop | Confirm that autovectorization—the compiler's automatic conversion of a scalar loop into vector operations—happened, used the intended width, avoided helper calls, and did not regress after a source or compiler change. |
| SIMD crates | Portable vector types can remove some target-specific syntax, but they do not ordinarily make Rake's combined one-register, no-spill, no-call, and no-replacement-with-scalar-operations contract a condition of compilation. |
| C / Rust intrinsics | Maintain target-width implementations and dispatch. Intrinsics are compiler-provided functions for requesting particular machine operations; the compiler still owns register allocation and may spill intermediate vectors. |
| Rake native profile | Select the target profile. The compiler either proves the advertised machine properties or rejects the program. |
run/pack traversal and the SSE2 and
AVX-512 backends remain under construction.
Ordinary rack arithmetic performs the same operation in every lane. Conditional work needs a way to say which lanes participate without turning the rack back into a scalar loop.
A tine names a boolean choice for every lane. A through block computes a candidate where that tine is true. A sweep selects the first matching candidate for each lane and requires a final catch-all value, so every lane has a result.
Rake writes a tine as #active. The hash resembles a perforated mask: values
fall through its open lanes and are held back by the rest. The mark has one role wherever
the mask is declared or used.
A binding gives a computed value a name. A later reference tells the compiler to use that value again. Neither construct creates a pointer or requires a memory load. Physical storage is a separate compiler decision: a live value can occupy a register, or an ordinary compiler can temporarily write it to memory and reload it later. That temporary write is a spill.
crunch advance(positions: f32s, velocities: f32s) -> f32s: | scaled: f32s <| velocities * <0.5> | result: f32s <| positions + scaled return result
<| exists only in a fused-flow binding. Read it from right to
left: the expression flows into the name. Consecutive fused-flow bindings form one pure,
connected machine-level computation. Rake must keep that region inline, call-free,
spill-free, and free of hidden scalar work, or reject the definition.
Every accepted rack already obeys Rake's one-register and no-spill rules, including racks
introduced with an ordinary let. Fusion adds a stronger boundary around the
whole chain: its operations must remain pure, connected, and contiguous through lowering.
The compiler cannot interrupt the chain with memory operations or an unknown helper call.
scaled is a readable alias inside the fused graph. It creates no storage,
evaluation point, or rounding boundary, so the compiler immediately sees the graph as
positions + velocities * <0.5>. The backend then chooses the cheapest legal
target operations. On AVX2 and AArch64 NEON, this graph becomes one native fused
multiply-add and the dead multiply disappears.
The language contract also permits the backend to reassociate, factor, distribute, eliminate common expressions, and form other fused instructions when its target cost model finds a cheaper graph. The current alpha implements the first useful slice: transparent alias substitution, dead-intermediate removal, and multiply-add contraction. The broader algebraic optimizer remains under construction.
fma(a, b, c) is needed only when correctness specifically depends on one fused
multiply-add operation with one rounding step. Rake provides no slower arithmetic mode for
preserving instruction boundaries that ordinary operators did not request. Named strict
reductions and scans retain their published ascending-lane order; fusion does not silently
reassociate those operations.
These complete programs use the canonical source language. Rake reports each construct's implementation status separately: frontend acceptance establishes syntax and types, while a production target additionally requires instruction selection, register assignment, assembly, executable comparison, and inspection of the encoded machine code.
A crunch applies one expression to every lane. Here <0.5>
is an explicit scalar broadcast. Each intermediate name is assigned once, which makes
the data dependencies explicit and lets the compiler check the whole sequence as one
fused region. The corresponding straight-line operation set is implemented by the
Rake-owned x86-64 AVX2 backend and verified after assembly.
crunch advance(positions: f32s, velocities: f32s) -> f32s: | scaled: f32s <| velocities * <0.5> | result: f32s <| positions + scaled return result
The corresponding masked operation is verified by the AVX2 backend. The #valid tine guards
sqrt, and the final _ arm defines all unmatched lanes. Native
lowering explicitly replaces inactive operands with benign values before partial
operations. The emitted function uses AVX2 vector masks and blends without scalar lane
branches.
rake safe_root(values: f32s) -> f32s:
tine #valid when values >= <0.0>
through #valid else <0.0> into rooted:
sqrt(values)
return sweep:
| #valid => rooted
| _ => <0.0>
Canonical language contract; native traversal remains in flight.
A stack groups columns by their compact storage types. A pack
supplies the complete columns to run, and using f32s
selects one physical rack and its logical lane count. up to <count> makes the
uniform record bound explicit. A narrower column cannot enter
arithmetic until widen converts its values while preserving that lane count.
The frontend contract covers the declarations, widening, and loop shape. Native memory
traversal, safe final partial racks, and the machine-level convention for calling the
function from C—its ABI—are still under construction.
stack Samples {
f32: value;
u8: quality;
}
run scale_values(
input: pack Samples,
<count: i64>,
<scale: f32>
) -> f32:
for chunk in input using f32s up to <count>:
let quality: u32s = widen(chunk.quality)
yield chunk.value * <scale>
Intel's ISPC is a mature C-like language for writing one program that runs across a group of CPU vector lanes. ISPC and Rake therefore belong to the same useful language family: both let programmers write one lane-parallel program and retarget it across hardware widths. ISPC presents this single-program, multiple-data (SPMD) model through an imperative C-family language. Rake explores an expression-oriented, dataflow-heavy syntax with a stricter compile-or-reject machine contract. More than one good design for this problem is an advantage for systems programmers.
Both examples update every particle in a structure-of-arrays dataset. ISPC traverses the columns in compile-time gangs, while Rake traverses them in target-sized racks.
export void advance_x(
uniform float position_x[],
uniform float velocity_x[],
uniform float output_x[],
uniform int count,
uniform float dt) {
foreach (i = 0 ... count)
output_x[i] = position_x[i]
+ velocity_x[i] * dt;
}stack Particles {
f32: position_x, velocity_x;
}
run advance_x(
particles: pack Particles,
<count: i64>,
<dt: f32>
) -> f32:
for particle in particles using f32s up to <count>:
yield particle.position_x
+ particle.velocity_x * <dt>
ISPC keeps the familiar loop, index, assignment, pointer, and C interoperability model.
Values inside the gang are varying unless marked uniform. Rake moves the
traversal and columnar schema into for … using … up to …, pack, and
stack, while angle brackets mark scalar values. Rake's core vector forms
favor pure expressions and SSA-like bindings, although Rake is not defined as a purely
functional language.
| Design choice | ISPC | Rake |
|---|---|---|
| Syntax and mental model | Imperative C-family SPMD with loops, assignments, pointers, functions, and familiar control flow. | Expression-oriented vector dataflow with explicit scalar markers, fused bindings, tines, through blocks, and total sweeps. |
| Parallel unit | A gang of program instances. The selected gang may be wider than one native CPU vector. | A CPU rack is exactly one native vector register; the proposed GPU mapping is described in the next section. |
| SoA model | soa<n> creates an explicitly n-wide SoA representation and supports layout conversion. | stack declares columns; pack supplies runtime storage and count; the target profile selects the traversal width. |
| Divergence vocabulary | Ordinary if, loops, and function calls acquire uniform or varying behavior from their operands. | Named tines expose masks; safe through blocks produce candidates; a total sweep selects each lane's result. |
| CPU machine contract | ISPC preserves gang semantics while the LLVM compiler infrastructure owns instruction selection and register allocation. | Rake additionally rejects splitting, scalar lane loops, rack spills, and helper calls wherever the selected profile forbids them. |
| Backend strategy | LLVM-based native CPU compilation plus documented Intel Xe GPU targets. | Rake-owned CPU instruction selection and allocation; a proposed cross-vendor GPU backend with a separate driver trust boundary. |
| Maturity | Established production compiler, broad language, C/C++ interoperability, tasking, libraries, tools, and deployed CPU/GPU targets. | Experimental alpha with verified AVX2 and NEON register-level slices; pack traversal, broader CPU profiles, and GPU emission remain unfinished. |
ISPC language and target guide → ISPC for Intel Xe →
ISPC is neither abandoned nor a mainstream application language. It is a successful specialist compiler used for performance-critical kernels beside larger C and C++ systems. OSPRay's CPU renderer is based on ISPC and uses SSE4, AVX, AVX2, AVX-512, and Arm NEON. Embree supports ISPC for parallel renderer code, and CMake has recognized ISPC as a compilation language since version 3.19.
That placement explains the small public profile. An application developer encounters
OSPRay or Embree while the .ispc files remain an implementation detail.
Rendering and scientific visualization also produce less general-language discussion
than CUDA, shader languages, C++ intrinsics, or Rust SIMD libraries. Intel's name can
suggest a narrower hardware target than ISPC's CPU support, which includes x86 and Arm.
OSPRay CPU implementation → Embree ISPC support → CMake ISPC language support →
Rake does not claim to have invented portable SPMD-on-SIMD. ISPC established that one lane-parallel program can be productive across CPU vector widths, and its mature language, tooling, interoperability, and deployments are substantial advantages.
ISPC guarantees the program's SPMD semantics and generally emits excellent vector code. Its performance guide nevertheless documents cases where irregular accesses require non-contiguous vector loads and stores called gathers and scatters, or one-at-a-time serialized loads. Those are legitimate implementations of ISPC semantics. Rake's narrower contribution is to let a selected profile forbid such implementations and turn the physical lowering into an acceptance condition.
Rake can carry its fail-closed philosophy into GPU compute, but the CPU promise cannot be copied word for word. Vulkan runs many copies, or shader invocations, of a GPU program. A subgroup is a set of those invocations that execute together and exchange values directly. A CPU rack is one value held in one SIMD register; a GPU rack would be one logical subgroup, with one lane owned by each invocation.
On a CPU, Rake controls instruction selection, register assignment, textual assembly, and the resulting native object file. For Vulkan GPUs, Rake would instead emit SPIR-V, a portable binary program that a hardware vendor's driver later translates into device-specific instructions. Rake can therefore verify the SPIR-V module and its declared requirements, but the driver owns the final GPU instructions and register allocation.
A SPIR-V vector is normally a small two-, three-, or four-component value inside one shader invocation. A Vulkan subgroup is a set of invocations that can exchange data and execute collective operations efficiently. Hardware commonly executes those invocations together as a wave or warp, although Vulkan defines subgroup behavior rather than a vendor instruction format.
A Rake GPU profile should map one logical rack lane to one subgroup invocation. Rack
addition then becomes one scalar addition in every invocation. A rack reduction becomes
a SPIR-V subgroup reduction. A shuffle becomes a subgroup shuffle. Tines become explicit
per-invocation predicates. Treating a 32-lane rack as OpTypeVector 32 would
describe the wrong execution model.
| Property | CPU rack | GPU rack |
|---|---|---|
| Physical interpretation | One native SIMD register | One logical subgroup spread across shader invocations |
| Lane storage | One component of the register | One scalar value owned by each invocation |
| Width selection | Element type and CPU profile | Required subgroup size in the Vulkan pipeline profile |
| Portable evidence | Register-assigned program and encoded instructions | Validated SPIR-V, declared capabilities, and pipeline requirements |
| Final register allocation | Owned by Rake | Owned by the Vulkan driver |
The dataset contains 400 particle elements. One GPU rack covers one logical subgroup's
worth of those elements, with one element assigned to each active invocation. A
vulkan-subgroup32 profile therefore traverses the dataset as twelve complete
racks and one rack with sixteen active invocations. A vulkan-subgroup64 profile
uses six complete racks and one with sixteen active invocations. Vulkan dispatch is rounded
to the configured workgroup size, the larger programmer-declared batch
that may contain several subgroups. Rake's generated bounds predicate prevents padded
invocations from touching memory or performing unsafe partial operations.
| Selected profile | Logical rack width | 400-particle decomposition |
|---|---|---|
| x86-avx2 | 8 values | 50 complete racks |
| vulkan-subgroup32 | 32 invocations | 12 complete + 16 active |
| vulkan-subgroup64 | 64 invocations | 6 complete + 16 active |
A named Vulkan profile would include a SPIR-V version, required Vulkan features, subgroup width, floating-point modes, memory model, and permitted operation set. Pipeline creation would check the physical device and reject a profile whose required subgroup size or capabilities are unavailable.
Vulkan drivers compile SPIR-V into device-specific executables during pipeline creation. The driver may change instruction selection, schedule operations, allocate registers, or spill values. Consequently a portable Rake SPIR-V profile cannot promise any of the following properties solely from the emitted module:
Rake could recover a stronger guarantee for a named physical device and driver. A certification tool would create the Vulkan pipeline, capture executable statistics and any exposed internal representation, and apply a vendor-specific checker for spills, register use, helper code, and prohibited instructions. The certificate would bind the SPIR-V hash, pipeline state, device identity, driver version, subgroup size, and checker version.
Vulkan exposes pipeline executable statistics and internal representations through an optional developer-tools extension. Those representations are implementation-dependent and may include final shader assembly. A missing extension or unavailable assembly would make certification fail rather than weaken the claim. The resulting certificate would prove properties for one device and driver combination; it would not extend to every device that accepts the SPIR-V module.
Shader languages such as GLSL and HLSL, as well as ISPC and other compilers, can emit excellent SPIR-V, including the subgroup operations described above. Rake's prospective advantage is the compilation contract around those operations: the same lane semantics used for CPU racks, explicit SoA traversal and predication, named reproducible GPU profiles, and rejection whenever the emitted SPIR-V cannot preserve the selected profile.
Rake's combined contract would let one kernel express CPU SIMD and GPU subgroup execution without relying on CPU autovectorization or accepting silent GPU emulation. The advantage comes from validation and refusal. SPIR-V already has the necessary expressive capability.
SPIR-V specification → Vulkan subgroup model → Pipeline executable inspection →
ISPC is Rake's closest established relative. Several neighboring systems solve parts of the CPU-and-GPU problem at a different level of abstraction or with a different machine contract.
| Language family | What it provides | How its contract differs |
|---|---|---|
| Halide | A functional, C++-embedded language for image and stencil pipelines with separate algorithm and scheduling choices across CPUs and GPUs. | Vector widths, tiling, placement, and other scheduling decisions are supplied by a programmer or autoscheduler rather than certified as Rake-style physical rack obligations. |
| Futhark | A statically typed, purely functional array language that compiles data-parallel programs to CUDA, OpenCL, or multithreaded CPU code. | Its central abstraction is bulk and nested array parallelism rather than one target-sized rack that must remain one CPU register. |
| CUDA, HIP, shader languages, and Slang | GPU-oriented SPMD or SIMT programming over threads, warps, waves, and subgroups, with targets such as portable SPIR-V, Microsoft's DXIL format, and native GPU code. | They expose the GPU execution model well, but do not make one source construct a portable CPU SIMD register with Rake's native fail-closed contract. |
| Taichi | A Python-embedded, data-oriented DSL that compiles parallel kernels for multicore CPUs and GPUs. | It concentrates on portable parallel iteration and data layout rather than proof of a fixed physical SIMD representation. |
| Mojo | A systems and AI language with explicit parameterized SIMD values, vectorized algorithms, and CPU/GPU facilities. | The vector width is part of the SIMD type, and Mojo documents that an oversized value may be split across registers. Rake's rack width comes from the profile and splitting is a rejection condition. |
| C++ and Rust SIMD libraries | Portable vector types and algorithms reduce direct use of architecture-specific intrinsics. | They remain library interfaces and do not ordinarily combine one-register identity, no spills, no helper calls, and no scalar lane loops into a compiler acceptance rule. |
| Research array and staging languages | Impala/AnyDSL, Lift/Shine, Accelerate, and Dex explore staged compilation, rewrite systems, and functional data-parallel programming. | They are valuable prior art, but none is a widely deployed direct replacement for ISPC's C-family SPMD-on-SIMD niche. |
The language contract includes native-backed operations, forms whose implementations stop in the frontend, and targets that remain design work. The table identifies each implementation boundary in version 0.3.0-alpha.1.
| Area | 0.3.0-alpha.1 status |
|---|---|
| Native CPU | Rake-owned x86-64 AVX2 and AArch64 NEON backends for supported crunch and predicated rake programs, including uniform 32-bit floating-point (f32) parameters, no-spill allocation, textual assembly, and post-assembly verification |
| Language contract | run, pack traversal, stacks, packs, and explicit widening are canonical source constructs; the capability report identifies the frontend and native subset implemented by this build |
| Other targets | AVX-512, SSE2, and GPU native backends remain unimplemented |
| C boundary | The tested alpha crunch boundary uses the eight vector-argument registers provided by the System V x86-64 calling convention; no production run wrapper or stable cross-release ABI yet |
| Performance | No representative published benchmark or generated-code performance claim |
| Future syntax | Some forms parse for design work but fail with source-located WIP: diagnostics |
The compiler's --print-capabilities report describes the parser and type
checker's accepted language surface. The native backend rejects every construct it cannot yet lower without
violating rack identity, mask safety, fusion, or no-spill allocation. Native crunches and
predicated rakes substitute fused aliases and optimize ordinary multiply-add graphs into
native FMA instructions. Source-level fma(...) requires the same
one-rounding arithmetic when correctness depends on it. Broader reassociation, factoring,
distribution, and common-expression optimization remain roadmap work. AVX2 also supports strict
f32 reductions, which combine all lanes into one value, and inclusive scans,
which produce every running partial result. Records, tuples, lane rearrangement, lambdas,
and expression pipelines remain unavailable.
Rake is written in the OCaml programming language and built with its Dune build system. The repository uses Nix to provide pinned versions of those tools and the system assembler. These commands build rakec and verify one supported crunch through the native AVX2 object-code gate.