RISC-V Atomics and the Vector Extension

vsetvli and the Four Fields It Carries

27 minadvancedModule 2: The Configuration Instruction, and the Data Path It ConfiguresContinues from rv-fence
27 min · Concept 3 of 4 · module: vector · RISC-V Atomics and the Vector Extension

A 32-bit word that describes a vector of any length

MEASURED-ON-BYTES, and this is the sharpest encoding result in the course. One fixed 32-bit instruction describes a vector, and the length is not in this instruction at all.

That sounds like a defect and it is the design. vsetvli carries four things — the element width, the register-group multiplier, and the tail and mask policies — and it carries a fifth thing that is not a field but an absence: the length. The requested length is an AVL and it arrives in inst[19:15]; the length that comes out lives in a CSR the instruction does not contain. So the instruction is a description and not a value: it says what kind of vector to use, and the machine says how many elements that is. That is how one encoding serves a machine with 128-bit vectors and a machine with 1024-bit vectors — there is nothing in the word to disagree about.

Compare this to the other two designs and the difference is in kind again. A wider register file puts the length in the hardware and the compiler in the loop. A mask register puts it in a named register. RISC-V puts it in a readable CSR, so a program that guessed wrong finds out at run time instead of reading a number the hardware never told it. That is a design decision about what to make observable, and it is the reason portable vector code on this architecture is possible at all.

The compiler proves the claim rather than the page asserting it, and it does it in the most ordinary instruction there is:

$ python3 rvat.py --run | sed -n '/PROVES THE LENGTH IS NOT/,/csrr t0/p' | head -7
    -O2  vadd_d   0xc22028f3  csrr a7, vlenb
    -O2  vmac_l   0xc22026f3  csrr a3, vlenb
    -O2  vmul_s8  0xc2202873  csrr a6, vlenb
    -O2  vsum_d   0xc22026f3  csrr a3, vlenb
    -Os  vmac_l   0xc22022f3  csrr t0, vlenb
    -Os  vsum_d   0xc22028f3  csrr a7, vlenb
                

csrr a7, vlenb — a CSR read, an ordinary Zicsr instruction, the same encoding as csrr a7, cycle with a different number. And the CSR number is measurable, and it lands in the range the privileged course already decomposed:

    0xc22028f3  csrr a7, vlenb
      csr = inst[31:20] = 0xc22   funct3 = 2 (CSRRs)   rd = 17
      csr[11:10] = 0b11  accessibility: READ-ONLY
      csr[9:8]   = 0b00  lowest privilege: U
                

Read-only from user mode, and the manual calls the value a design-time constant in any implementation. What VLEN is on a machine this course has never seen is therefore not a number this course has, and a VLEN of 128 and a VLEN of 1024 would both produce exactly this assembly. That is not a gap in the measurement; it is the design working.

Four fields, each isolated by an XOR

The map of the eleven-bit type field, measured rather than read off a diagram: one variant is the base and each row changes exactly one thing, so the XOR is the field.

$ python3 rvat.py --run | sed -n '/THE FOUR FIELDS, EACH ISOLATED/,+8p'
  the variant      zimm[10:0]   XOR against e8/m1/tu/mu  bits moved  which
  e8, m1, tu, mu   00000000000  0x00000000               0
  e16, m1, tu, mu  00000001000  0x00800000               1           bit 23
  e32, m1, tu, mu  00000010000  0x01000000               1           bit 24
  e64, m1, tu, mu  00000011000  0x01800000               2           bit 23, bit 24
  e8, m2, tu, mu   00000000001  0x00100000               1           bit 20
  e8, m8, tu, mu   00000000011  0x00300000               2           bit 20, bit 21
  e8, m1, ta, mu   00001000000  0x04000000               1           bit 26
  e8, m1, tu, ma   00010000000  0x08000000               1           bit 27
                

Six of the twelve probes move a single bit — one per field — and the rows that move two or three are the rows changing a three-bit field at once. The field map that falls out is exactly what the specification draws, and it is worth drawing out because of the reserved bits:

      vma       zimm[7]      one bit, the MASK policy
      vta       zimm[6]      one bit, the TAIL policy
      vsew[2:0] zimm[5:3]    three bits, the ELEMENT WIDTH
      vlmul[2:0]zimm[2:0]    three bits, the REGISTER GROUP
      RESERVED  zimm[10:8]   THREE BITS THAT NO REACHABLE vsetvli USES

  MEASURED: zimm[10:8] is 0b000 in all 120 `vsetvli` in the corpus
                

One hundred and twenty reachable configurations in the corpus, four element widths times seven register-group multipliers times two tail policies times two mask policies, and three bits of the eleven-bit field are constant zero across every one of them. The manual agrees and says what they are for — the vill bit sits above them and the whole block is reserved if non-zero.

Three bits that never move look exactly like three bits that do. A field map measured by sweeping records which bits move; a field map read off a diagram records which bits are set in somebody’s example. The sibling encoding course measured vsetvli at four bytes and found the same three bits constant, and this page builds on that rather than repeating it — but the generalisation is the point: a field map measured by sweep records which bits move, and a field map copied from a table records which bits someone used.

And it is not waste. The reserved bits are at the top, with vill above them, so a future extension that wants a field gets the space without moving any bit anyone already uses — which is what a fixed 32-bit encoding has to be designed for and what x86-64 could not do, because its instruction length is variable and its prefixes are not fields at all.

Two bits of register identity carrying a three-way choice

The length is not the only thing the instruction does not contain. So is the meaning of inst[19:15], and the same field is a register in two instructions and an immediate in the third:

$ python3 rvat.py --run | sed -n '/THE THREE CONFIGURATION INSTRUCTIONS/,+4p'
  word        the instruction, as it disassembles  inst[31:30]  inst[19:15]  at
  0x0c55f2d7   vsetvli    t0, a1, e8, mf8, ta, ma  0b00        11          0x0
  0xcd0072d7   vsetivli   t0, 0x0, e32, m1, ta, ma  0b11        0           0x8
  0x80c5f2d7   vsetvl     t0, a1, a2               0b10        11          0x1c
                

Three instructions, told apart by two bits and by nothing else: inst[31:30] of 00 is vsetvli, 10 is vsetvl, 11 is vsetivli. The other twenty-nine bits are laid out the same way in all three. The table’s last column is the address in the committed listing, so a reader with a hex editor and no cross-compiler can find the same three lines.

And the harness checks that column against the words themselves — 129 configuration rows, every one’s printed mode compared with the mode decoded from its own word. A row claiming vsetvl at 0b00 would be a table saying the opposite of the sentence three lines above it, and a table nobody parses is a table that can be edited into a different claim without anything noticing.

Now the two-bit register argument again, on the AVL side. Chapter 30’s Table 49 has three cases, and which one you get is decided by whether two registers are zero:

    rd    rs1   AVL value              Effect on vl
    -     !x0   Value in x[rs1]        Normal strip mining
    !x0   x0    ~0                     Set vl to VLMAX
    x0    x0    Value in vl register   Keep existing vl
                

Three rows, three different meanings, and the mechanism is two bits of register number — which is the same kind of argument as inst[31:30] choosing the instruction, and it is why no decoder can treat rd and rs1 as ordinary operands without reading which instruction it is looking at first. A register number is normally a name; here it is a switch.

And the compiler uses the VLMAX row: of the 120 vsetvli in the corpus, 117 name a register and 3 ask for VLMAX by putting x0 in rs1. So the shape the compiler prefers is the one where the value is not in the instruction — which is the whole design again, one instruction deeper.

The tail policy: where portable vector code goes to get hard

Two independent bits, four combinations, and the one on the right is “agnostic” — which means the hardware may leave the tail alone or may overwrite it with all ones, at its discretion. And the manual is explicit that the pattern need not even be repeatable:

    rv32-unpriv, chapter 30, section 31.3.4.3:
        vta vma  Tail Elements      Inactive Elements
         0   0    undisturbed       undisturbed
         0   1    undisturbed       agnostic
         1   0    agnostic          undisturbed
         1   1    agnostic          agnostic
                

That is a library contract made of bits, and it is the one place in the vector extension where the answer is “the hardware may do either”. The specification says each destination element can be either left undisturbed or overwritten with ones, in any combination, and that the pattern is not required to be deterministic when the instruction is executed with the same inputs. Portable vector code must therefore choose tu/mu unless it knows the target, and every performance-minded vector loop wants ta/ma — so the fast version is not the portable one, and the encoding is where that decision becomes visible.

And what the compiler does with it is measured, which is the part that should worry you:

$ python3 rvat.py --run | sed -n '/MEASURED, in the corpus/,+7p'
  tail  mask  how many of the corpus's vsetvli
  ta    ma    31
  ta    mu    29
  tu    ma    28
  tu    mu    32

  31 of 120 are tail-AGNOSTIC and mask-AGNOSTIC.
                

Thirty-one of a hundred and twenty are the fully agnostic pair. And that is the fast choice, not the portable one, so the compiler is making a non-portable choice in its default output and the only way a reader learns it is by decoding two bits. This is why the neutral course on vector boundaries spends a page on the remainder problem: on an architecture where the tail policy is unspecified, a tail loop you wrote yourself is a portability decision you made without being asked.

There is one more piece of the story and it is about the assembler rather than the hardware. The specification used to make the flags optional with a default, then made them mandatory, and the artifact prints both the change and the reason it was a good change:

The defaults changed and the change was made by removing the default. The specification’s own suggestion had been that the default should perhaps be the agnostic pair; the assembler disagreed and kept the historical undisturbed default, and then a later version deprecated the omission entirely. Removing a default is a better answer than changing one: a program that says nothing now gets a diagnostic, and a program that says tu means it. That is the only way two versions of a specification can disagree without a compiler being unable to tell which is which — and the three refusals that show it are in the artifact, because a rule you can see refused is a rule you can trust.

Try it

  1. Find the bits that never move.(The artifact swept 112 vsetvli variants and found zimm[10:8] constant. Do it yourself: assemble vsetvli t0, a1, e8, m1, tu, mu and vsetvli t0, a1, e64, m1, tu, mu and XOR the two words. The bits that change are the width. Now ask which bits in the eleven-bit field you have not moved, and what would have to be true of the hardware for them to be reachable at all.)
  2. Ask the assembler about a field that means two things.(Assemble vsetvli t0, a1, e8, m1, tu, mu and vsetivli t0, 0x1f, e8, m1, tu, mu and XOR them. The bits that move are in inst[19:15] — the field the base ISA calls rs1. Now read the base ISA’s field map for inst[19:15] and notice it is right and wrong at the same time, depending on which instruction you are looking at. A field whose meaning is a function of a neighbour is not a register.)
  3. Count the agnostic tails in a real build.(Compile a vectorised loop and grep the disassembly for ta, ma against tu, mu. Then read what the specification says the second one guarantees and the first one does not. You have just found a portability decision your compiler made for you, and the only way to see it is to decode two bits of an eleven-bit field. That is the cost of the design, and it is cheap until you move the binary.)

Connect

What a vector lane is and what a reduction is — in neutral terms, with no architecture attached — is simd-width and simd-reduce. Why a vectorised loop needs a remainder, and why that is hard to do portably, is simd-boundaries, and this page is the architecture-specific answer to the question it asks. What the vectoriser does and does not do is simd-compiler; the same subject on the other two architectures is a64-neonspace, where the length is in the instruction and the answer is different for a reason you can now state. And the encoding course’s rv-encoding is where vsetvli at four bytes with three constant-zero bits was first measured — this page builds on that and does not repeat it.

Next: Decode the Data Path — because a configuration instruction that sets a policy is not much use until you can see what the policy does to a load, and the next page reads the data path a byte at a time.