모든 이야기

vector-length agnostic: 열 개를 5개씩 나누어도 빠뜨리지 않는 루프

32비트 정수 10개에 각각 1을 더하는 벡터 루프를 만들었다. 한 코어는 벡터 레지스터 한 개에 4개를 담고, 다른 코어는 8개를 담는다. 코드를 두 벌 만들지 않고 모두 처리하려면, 루프가 “이번에는 몇 개를 처리했는가”를 하드웨어에서 받아야 한다. 이것이 vector-length agnostic 코드의 출발점이다.

최대 용량과 이번 작업량은 다르다

RISC-V 벡터 명령에서 VLEN은 벡터 레지스터 한 개의 비트 길이, SEW는 설정한 원소 폭이다. LMUL=1로 한 레지스터를 사용하면 SEW=32에서 VLEN=128인 구현의 VLMAX는 4, VLEN=256인 구현에서는 8이다. 여기서 VLMAX는 설정상 한 번에 다룰 수 있는 최대 원소 수이고, 실제 작업량은 vl이다. RISC-V Vector, Vector Register Grouping 및 Vector Length Register

프로그램은 vsetvli에 남은 원소 수를 AVL로 전달한다. 명령은 해당 설정에서 사용할 vl을 정하고, 목적지 정수 레지스터에도 그 값을 돌려준다. 프로그램이 처리 개수를 고정해 추측하지 않고 이 반환값을 사용하는 것이 핵심이다.

예제는 두 구현이 e32,m1과 아래 명령을 지원하며, 입력·출력 배열의 주소가 유효하고 두 배열이 겹치지 않는다고 가정한다. a0에는 남은 원소 수, a1과 a2에는 각각 입력·출력 주소가 들어 있다.

    beqz a0, done
loop:
    vsetvli t0, a0, e32, m1, ta, ma
    vle32.v v0, (a1)
    vadd.vi v0, v0, 1
    vse32.v v0, (a2)
    slli t1, t0, 2
    add a1, a1, t1
    add a2, a2, t1
    sub a0, a0, t0
    bnez a0, loop
done:

t0가 이번 vl이다. 32비트 정수는 4바이트이므로 주소는 4×t0만큼 전진하고, 남은 수에서는 t0를 뺀다. 덧셈은 각 활성 원소에 독립적으로 적용된다. mask를 쓰지 않으므로 v0를 데이터 레지스터로 사용한다. ta,ma 설정에서 비활성 영역이 어떤 유효 결과를 가진다고 기대하지 않으며, 저장도 이번 활성 원소만 대상으로 한다.

4개씩과 8개씩은 가능한 실행 예다

10개를 VLMAX=4인 구현이 4,4,2개로 처리한다고 하자. 첫 반복은 인덱스 03을 쓰고 16바이트 전진한다. 두 번째는 47을 쓰고 다시 16바이트 전진하며, 마지막은 8~9를 쓰고 8바이트 전진한다. 원소마다 정확히 한 번 1을 더한다.

VLMAX=8인 구현이 8,2개로 처리하면 첫 반복에서 07, 다음에서 89를 다룬다. 반복 횟수는 달라도 같은 입력 배열에 대한 결과는 같다. 두 추적은 규약이 허용하는 예이며, 모든 구현이 반드시 이 묶음을 택한다는 뜻은 아니다.

여기서 흔한 함정이 있다. vl이 항상 min(AVL,VLMAX)라고 단정하는 것이다. 원문은 AVL이 VLMAX보다 크고 2×VLMAX보다 작을 때, ceil(AVL/2)부터 VLMAX까지의 값을 허용한다. 남은 두 반복의 작업을 더 고르게 나누기 위해서다. RISC-V Vector, Constraints on Setting vl

따라서 AVL=10, VLMAX=8에서는 첫 vl이 5인 구현도 가능하다. 그 경우 추적은 5,5가 된다. 위 코드는 t0=5를 받아 주소를 20바이트 전진시키고 남은 수를 5로 바꾸므로 정상 동작한다. 첫 반복에서 8개를 처리했다고 가정해 주소를 32바이트 전진시키면, 실제로는 인덱스 04만 처리한 뒤 57을 건너뛰게 된다.

반환값 하나가 경계를 책임진다

vl 설정 규약에는 종료를 뒷받침하는 조건도 있다. AVL이 VLMAX 이하이면 vl은 AVL과 같고, 양의 AVL에서는 양의 vl이면서 AVL을 넘지 않는다. AVL이 2×VLMAX 이상이면 vl은 VLMAX다. 따라서 지원되는 설정에서 반환된 vl로 남은 수를 줄이면 마지막 조각까지 처리할 수 있다.

이 방식은 큰 배열을 하드웨어 용량에 맞는 조각으로 나누는 strip mining이다. 별도로 마지막 2개만 처리하는 scalar 루프를 반드시 만들 필요가 없고, 하드웨어 용량이 달라져도 같은 구조를 쓸 수 있다. RISC-V 원문의 예제 역시 반환된 vl에 원소 크기를 곱해 주소를 갱신한다. RISC-V Vector, Example of strip mining and changes to SEW

물론 결과 호환성이 성능까지 같게 만들지는 않는다. 더 긴 레지스터를 가진 코어가 실제로 더 많은 원소를 한 사이클에 계산하는지는 실행 lane, 메모리 대역폭과 명령 지연에 달렸다. 예제처럼 독립적인 원소 덧셈 대신 부동소수점 합산 순서를 바꾸는 알고리즘이라면 수치 결과의 별도 조건도 검토해야 한다.

vector-length agnostic 루프는 하드웨어 길이를 모른 채 무작정 실행하는 루프가 아니다. 매 반복마다 지원되는 설정에서 실제 작업량을 받고, 그 수만큼 계산·주소·잔여량을 함께 갱신한다. 10개를 4,4,2로 나누든 5,5로 나누든 빠뜨리지 않는 이유는 넉넉한 최대 용량을 믿어서가 아니라, 반환된 vl을 진행의 기준으로 삼기 때문이다.