# SV Overview [[discussion]] page feel free to add comments, questions. [[!toc]] This document provides an overview and introduction as to why SV (a Cray-style Vector augmentation to OpenPOWER) exists, and how it works. # Introduction: SIMD and Cray Vectors SIMD, the primary method for easy parallelism of the past 30 years in Computer Architectures, is [known to be harmful](https://www.sigarch.org/simd-instructions-considered-harmful/). SIMD provides a seductive simplicity that is easy to implement in hardware. With each doubling in width it promises increases in raw performance without the complexity of either multi-issue or out-of-order execution. Unfortunately, even with predication added, SIMD only becomes more and more problematic with each power of two SIMD width increase introduced through an ISA revision. The opcode proliferation, at O(N^6), inexorably spirals out of control in the ISA, detrimentally impacting the hardware, the software, the compilers and the testing and compliance. Cray-style variable-length Vectors on the other hand result in stunningly elegant and small loops, exceptionally high data throughput per instruction (by one *or greater* orders of magnitude than SIMD), with no alarmingly high setup and cleanup code, where at the hardware level the microarchitecture may execute from one element right the way through to tens of thousands at a time, yet the executable remains exactly the same and the ISA remains clear, true to the RISC paradigm, and clean. Unlike in SIMD, powers of two limitations are not involved in either the hardware or in the assembly code. SimpleV takes the Cray style Vector principle and applies it in the abstract to a Scalar ISA, in the process allowing register file size increases using "tagging" (similar to how x86 originally extended registers from 32 to 64 bit). ## SV The fundamentals are: * The Program Counter gains a "Sub Counter" context. * Vectorisation pauses the PC and runs a loop from 0 to VL-1 (where VL is Vector Length). This may be thought of as a "Sub-PC" * Some registers may be "tagged" as Vectors * During the loop, "Vector"-tagged register are incremented by one with each iteration, executing the *same instruction* but with *different registers* * Once the loop is completed *only then* is the Program Counter allowed to move to the next instruction. Hardware (and simulator) implementors are free and clear to implement this as literally a for-loop, sitting in between instruction decode and issue. Higher performance systems may deploy SIMD backends, multi-issue and out-of-order execution, although it is strongly recommended to add predication capability into all SIMD backend units. In OpenPOWER ISA v3.0B pseudo-code form, an ADD operation, assuming both source and destination have been "tagged" as Vectors, is simply: for i = 0 to VL-1: GPR(RT+i) = GPR(RA+i) + GPR(RB+i) At its heart, SimpleV really is this simple. On top of this fundamental basis further refinements can be added which build up towards an extremely powerful Vector augmentation system, with very little in the way of additional opcodes required: simply external "context". RISC-V RVV as of version 0.9 is over 180 instructions (more than the rest of RV64G combined). Over 95% of that functionality is added to OpenPOWER v3 0B, by SimpleV augmentation, with around 5 to 8 instructions. Even in OpenPOWER v3.0B, the Scalar Integer ISA is around 150 instructions, with IEEE754 FP adding approximately 80 more. VSX, being based on SIMD design principles, adds somewhere in the region of 600 more. SimpleV again provides over 95% of VSX functionality, simply by augmenting the *Scalar* OpenPOWER ISA, and in the process providing features such as predication, which VSX is entirely missing. AVX512, SVE2, VSX, RVV, all of these systems have to provide different types of register files: Scalar and Vector is the minimum. AVX512 even provides a mini mask regfile, followed by explicit instructions that handle operations on each of them *and map between all of them*. SV simply not only uses the existing scalar regfiles (including CRs), but because operations exist within OpenPOWER to cover interactions between the scalar regfiles (`mfcr`, `fcvt`) there is very little that needs to be added. In fairness to both VSX and RVV, there are things that are not provided by SimpleV: * 128 bit or above arithmetic and other operations (VSX Rijndael and SHA primitives; VSX shuffle and bitpermute operations) * register files above 128 entries * Vector lengths over 64 * Unit-strided LD/ST and other comprehensive memory operations (struct-based LD/ST from RVV for example) * 32-bit instruction lengths. [[svp64]] had to be added as 64 bit. These are not insurmountable limitations, that, over time, may well be added in future revisions of SV. The rest of this document builds on the above simple loop to add: * Vector-Scalar, Scalar-Vector and Scalar-Scalar operation (of all register files: Integer, FP *and CRs*) * Traditional Vector operations (VSPLAT, VINSERT, VCOMPRESS etc) * Predication masks (essential for parallel if/else constructs) * 8, 16 and 32 bit integer operations, and both FP16 and BF16. * Compacted operations into registers (normally only provided by SIMD) * Fail-on-first (introduced in ARM SVE2) * A new concept: Data-dependent fail-first * Condition-Register based *post-result* predication (also new) * A completely new concept: "Twin Predication" * vec2/3/4 "Subvectors" and Swizzling (standard fare for 3D) All of this is *without modifying the OpenPOWER v3.0B ISA*, except to add "wrapping context", similar to how v3.1B 64 Prefixes work. # Adding Scalar / Vector The first augmentation to the simple loop is to add the option for all source and destinations to all be either scalar or vector. As a FSM this is where our "simple" loop gets its first complexity. function op_add(RT, RA, RB) # add not VADD! int id=0, irs1=0, irs2=0; for i = 0 to VL-1: ireg[RT+id] <= ireg[RA+irs1] + ireg[RB+irs2]; if (!RT.isvec) break; if (RT.isvec) { id += 1; } if (RA.isvec) { irs1 += 1; } if (RB.isvec) { irs2 += 1; } With some walkthroughs it is clear that the loop exits immediately after the first scalar destination result is written, and that when the destination is a Vector the loop proceeds to fill up the register file, sequentially, starting at `rd` and ending at `rd+VL-1`. The two source registers will, independently, either remain pointing at `RB` or `RA` respectively, or, if marked as Vectors, will march incrementally in lockstep, producing element results along the way, as the destination also progresses through elements. In this way all the eight permutations of Scalar and Vector behaviour are covered, although without predication the scalar-destination ones are reduced in usefulness. It does however clearly illustrate the principle. Note in particular: there is no separate Scalar add instruction and separate Vector instruction and separate Scalar-Vector instruction, *and there is no separate Vector register file*: it's all the same instruction, on the standard register file, just with a loop. Scalar happens to set that loop size to one. # Adding single predication The next step is to add a single predicate mask. This is where it gets interesting. Predicate masks are a bitvector, each bit specifying, in order, whether the element operation is to be skipped ("masked out") or allowed. If there is no predicate, it is set to all 1s, which is effectively the same as "no predicate". function op_add(RT, RA, RB) # add not VADD! int id=0, irs1=0, irs2=0; predval = get_pred_val(FALSE, rd); for i = 0 to VL-1: if (predval & 1< Adding in support for SUBVL is a matter of adding in an extra inner for-loop, where register src and dest are still incremented inside the inner part. Predication is still taken from the VL index, however it is applied to the whole subvector: function op_add(RT, RA, RB) # add not VADD!  int id=0, irs1=0, irs2=0;  predval = get_pred_val(FALSE, rd); for i = 0 to VL-1: if (predval & 1< Swizzle is particularly important for 3D work. It allows in-place reordering of XYZW, ARGB etc. and access of sub-portions of the same in arbitrary order *without* requiring timeconsuming scalar mv instructions (scalar due to the convoluted offsets). With somewhere around 10% of operations in 3D Shaders involving swizzle this is a huge saving and reduces pressure on register files. In SV given the percentage of operations that also involve initialisation to 0.0 or 1.0 into subvector elements the decision was made to include those: swizzle = get_swizzle_immed() # 12 bits for (s = 0; s < SUBVL; s++) remap = (swizzle >> 3*s) & 0b111 if remap < 4: sm = id*SUBVL + remap ireg[rd+s] <= ireg[RA+sm] elif remap == 4: ireg[rd+s] <= 0.0 elif remap == 5: ireg[rd+s] <= 1.0 Note that a value of 6 (and 7) will leave the target subvector element untouched. This is equivalent to a predicate mask which is built-in, in immediate form, into the [[sv/mv.swizzle]] operation. mv.swizzle is rare in that it is one of the few instructions needed to be added that are never going to be part of a Scalar ISA. Even in High Performance Compute workloads it is unusual: it is only because SV is targetted at 3D and Video that it is being considered. Some 3D GPU ISAs also allow for two-operand subvector swizzles. These are sufficiently unusual, and the immediate opcode space required so large, that the tradeoff balance was decided in SV to only add mv.swizzle. # Twin Predication Twin Predication is cool. Essentially it is a back-to-back VCOMPRESS-VEXPAND (a multiple sequentially ordered VINSERT). The compress part is covered by the source predicate and the expand part by the destination predicate. Of course, if either of those is all 1s then the operation degenerates *to* VCOMPRESS or VEXPAND, respectively. function op(RT, RS):  ps = get_pred_val(FALSE, RS); # predication on src  pd = get_pred_val(FALSE, RT); # ... AND on dest  for (int i = 0, int j = 0; i < VL && j < VL;): if (RS.isvec) while (!(ps & 1<