Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Intra-Slice Chain

Intra-Slice Chain은 각 Slice의 데이터에 대해 요소별 연산, 이항 연산, Intra-Slice Reduce 연산을 독립적으로 수행합니다. 활성화와 정규화 같은 축약 이후의 처리를 담당합니다. 예를 들어 \(\operatorname{sigmoid}(XW + b)\) 계산에서는 \(XW\)가 Contraction Engine에서 실행되고, 덧셈과 sigmoid 활성화는 Intra-Slice Chain에서 실행됩니다.

인터페이스

아래 예시는 고정소수점 바이어스를 적용하고, 부동소수점 경로에서 sigmoid를 실행한 뒤(4-way로 좁혔다가 다시 넓힘), ReLU clip으로 마무리합니다. 이 예제가 계산하는 식은 \(output[a, b] = \max(\operatorname{sigmoid}(input[a, b] + 100), 0)\)입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2];

fn staged_pipeline<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
    .vector_init()
    .vector_intra_slice_tag(TagMode::Zero)
    // input + 100
    .vector_fxp(FxpBinaryOp::AddFxp, 100)
    // i32 → f32 (fixed-point, int_width = 31)
    .vector_fxp_to_fp(31)
    // Way8 → Way4 for the float path
    .vector_narrow_trim::<m![A % 2 # 4]>()
    // sigmoid(input + 100)
    .vector_fp_unary(FpUnaryOp::Sigmoid)
    // Way4 → Way8
    .vector_widen_pad::<m![A % 2 # 8]>()
    // f32 → i32
    .vector_fp_to_fxp(31)
    // max(sigmoid(input + 100), 0)
    .vector_clip(ClipBinaryOpI32::Max, 0)
    .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let c: CollectTensor<'_, _, i32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = staged_pipeline(c);
}

파이프라인

체인은 (위에서 보인 것처럼) vector_intra_slice_tag()로 시작하거나(vector_init() 직후 또는 Inter-Slice Reducer의 출력에서), Pair Mode에서는 vector_init() 직후에 vector_intra_slice_unzip()으로 시작합니다(아래 참조).

    #[primitive(VectorInitTensor::vector_intra_slice_tag)]
    pub fn vector_intra_slice_tag(
        self,
        branch: TagMode<D>,
    ) -> VectorBranchTensor<'l, T, D, Chip, Cluster, Slice, Time, Packet, Fresh, { VeOrder::IntraFirst }> {

    #[primitive(VectorInitTensor::vector_intra_slice_unzip)]
    pub fn vector_intra_slice_unzip<I: AxisName, SplitTime: M>(
        self,
    ) -> VectorTensorPair<'l, T, D, stage::Tag, Chip, Cluster, Slice, SplitTime, Packet> {

진입 후 체인은 아래의 파이프라인 단계를 정해진 순서대로 거칩니다. 소프트웨어는 필요한 단계만 연결하고 나머지는 건너뛰며, 예제에서는 Logic, FpDiv, Filter를 건너뜁니다. 각 행에는 체인 내 단계의 위치(#), 단계 이름(Stage), 해당 단계를 트리거하는 API 메서드(Method), 실행되는 way(Way, 사이클당 8개 또는 4개 요소), 피연산자를 받는지 여부(Operand)가 나열되어 있습니다. 타입 시스템이 모든 단계 전환을 컴파일 시점에 강제하므로, 메서드는 앞선 체인이 호환되는 상태에 도달한 뒤에만 호출할 수 있습니다. 단계별 세부 사항은 아래의 단계에 있습니다.

#단계메서드Way피연산자→ Inter-Slice Reducer
1항목vector_intra_slice_tag()Way8––
2Logicvector_logic()Way8예예
3Fxpvector_fxp()Way8예예
4FxpToFpvector_fxp_to_fp()Way8–예
5Narrowvector_narrow_split() / vector_narrow_trim()Way8 → Way4––
6Floatvector_fp_unary/binary/ternary()Way4예–
7IntraSliceReducevector_intra_slice_reduce()Way4––
8FpDivvector_fp_div()Way4예–
9Widenvector_widen_concat() / vector_widen_pad()Way4 → Way8–예
10FpToFxpvector_fp_to_fxp()Way8–예
11Clipvector_clip()Way8예예
12Filtervector_filter()Way8––

vector_reinterpret()는 의도적으로 표에서 제외했습니다. 이 함수는 어떤 단계도 차지하지 않으며 임의의 두 단계 사이에 위치할 수 있습니다(Reinterpret 참조).

각 단계는 8-way(사이클당 8개 요소) 또는 4-way(사이클당 4개 요소)로 실행됩니다. 부동소수점 Cluster는 처리량이 절반인 ALU를 체인의 나머지 부분과 균형 맞추기 위해 4-way로 실행됩니다. 따라서 부동소수점 경로를 사용하는 체인은 8-way로 진입하여 부동소수점 단계 앞에서 Narrow(vector_narrow_split 또는 vector_narrow_trim)를 호출하고, 그 뒤에 Widen(vector_widen_concat 또는 vector_widen_pad)을 호출하여 8-way로 돌아갑니다. 예제가 바로 이렇게 동작합니다. vector_narrow_trim, vector_fp_unary(Sigmoid), vector_widen_pad를 차례로 호출합니다.

체인은 (예시에서처럼) vector_final()로 빠져나가거나, vector_inter_slice_reduce()로 빠져나가 Inter-Slice Reducer에 넘깁니다. 두 출구 모두 8-way를 요구하므로, 활성 상태인 4-way 단계는 먼저 Widen을 거쳐야 합니다.

피연산자

이항 연산과 삼항 연산에는 두 종류의 슬롯이 있습니다. 하나는 스트림(진행 중인 텐서, 즉 메서드 체인의 self로, 체인에 의해 고정됨)이고, 다른 하나는 한두 개의 피연산자(추가 입력)입니다. 각 피연산자는 세 가지 출처 중 하나에서 옵니다.

출처예시설명
상수100, 2.5f32모든 요소에 브로드캐스트되는 스칼라
VRF 텐서&vrf_tensorVector Engine에 들어가기 전에 .to_vrf()로 미리 로드됨
스태시Stash앞선 체인 단계의 스냅샷으로, 아래에서 설명함

삼항 연산(FmaF)은 (operand0, operand1) 쌍을 받습니다.

VRF 피연산자는 인덱서가 Slice마다 읽으며, 인덱서는 여기에 세 가지 규칙을 부과합니다.

첫째, 피연산자의 Chip / Cluster / Slice는 스트림의 것과 같습니다. Slice s에서 연산은 Slice s가 가진 것을 읽으므로, 다른 파티션 아래의 피연산자는 다른 Slice의 데이터를 조용히 읽는 대신 컴파일 오류가 됩니다. 두 파티션이 같은 Slice를 기술하는 경우 VrfTensor::reshape는 피연산자를 스트림의 파티션 아래로 다시 기술합니다. 예를 들어 익명의 m![256] 아래에 복제된 피연산자가 이름 있는 축으로 파티션된 스트림에 공급되는 경우가 이에 해당합니다.

둘째, 한 번의 접근(Packet: 8-way에서는 8개 레인, 4-way에서는 4개 레인)은 인덱서의 한 단계이며, 인덱서가 제공하는 읽기 방식은 두 가지뿐입니다. 브로드캐스트는 한 주소에서 모든 레인에 값을 공급하고, 연속 읽기는 연속된 Element 셀을 Packet개 읽습니다. 따라서 패킷 축은 피연산자의 가장 안쪽에 있는 연속 축이거나 피연산자에 아예 없어야 합니다. 피연산자의 바깥쪽 축을 순회하는 패킷이나, 한 번의 접근 안에서 브로드캐스트 레인과 실제 레인을 섞는 패킷은 인코딩이 없으므로 거부됩니다.

셋째, 축 불일치의 두 방향은 서로 같지 않습니다. 피연산자에는 있고 스트림에는 없는 축은 그 축이 인덱싱하는 셀이 결코 읽히지 않으므로 the stream leaves operand axes unmatched와 함께 거부됩니다. 스트림에는 있고 피연산자에는 없는 축은 브로드캐스트이며, 이를 통해 하나의 피연산자가 스트림의 모든 행에 공급됩니다.

같은 연산 메서드는 인자 타입에 따라 서로 다른 출처를 받아들입니다.

.vector_fxp(FxpBinaryOp::AddFxp, 100)         // operand from constant
.vector_fxp(FxpBinaryOp::MulInt, &vrf)        // operand from VRF tensor
.vector_clip(ClipBinaryOpI32::Max, Stash)     // operand from stash (set earlier)

Stash 소스는 vector_stash()에서 나옵니다. vector_stash()는 진행 중인 텐서의 스냅샷을 만들어, 이후의 이항 또는 삼항 연산이 이를 Stash 피연산자로 다시 읽을 수 있게 합니다. 대표적인 용도는 max(f(x), x)와 같은 잔차 연결(residual connection)이나 스킵 연결(skip connection)입니다. 이 경우 원래의 x가 중간 단계를 거치는 동안 유지되어야 합니다. 스태시는 별도의 피연산자 소스로 제시되지만 실제로는 VRF를 기반으로 합니다. 현재 진행 중인 텐서의 일부가 VRF에 저장되고 그곳에서 다시 읽힙니다. Tensor Unit 호출이 스태시를 사용할 때마다 컴파일러는 실제로 스태시되는 데이터의 양과 관계없이 Slice당 1,024 B의 VRF 용량을 스태시용으로 보수적으로 예약합니다. 해당 호출이 미리 로드된 VRF 텐서도 RHS 피연산자로 읽는다면 이 예약을 고려하여 피연산자의 크기를 정하십시오. 스태시와 RHS는 8 KiB VRF를 공유하므로 RHS에 남는 용량은 Slice당 최대 7 KiB입니다. vector_stash()는 Stashable 단계(Branch, Logic, Fxp, Narrow, Fp, FpDiv, Clip) 중 어디에서든 호출하십시오. 스냅샷은 Tensor Unit 호출이 끝날 때까지 유지되며, Stash를 받는 이후의 이항 또는 삼항 호출에 전달됩니다. 이 슬롯은 한 번만 쓸 수 있고(두 번째 vector_stash()는 컴파일 타임 오류입니다) 타입이 지정되므로, f32 스태시는 f32 연산에만 전달됩니다. 쓰기와 읽기 사이에 변환이나 재해석이 있다면 먼저 이를 되돌려야 합니다. 스태시는 또한 한 번만 읽을 수 있습니다. 스태시는 이후의 연산 정확히 하나에만 전달됩니다(스태시를 읽으면 슬롯이 Occupied 상태를 지나가므로 두 번째 Stash 읽기는 컴파일 타임 오류입니다). 두 번 이상 읽어야 하는 값은 스태시에 넣지 않습니다. 그런 값은 여러 번 읽을 수 있는 VRF에 넣고 &vrf로 전달하십시오. 매핑은 진행 중인 텐서를 따라가므로, Narrow 이전에 만든 스태시는 Widen 이후에도 여전히 사용할 수 있습니다. 이 일곱 단계는 VRF 쓰기 포트를 가진 일곱 개의 체인 단계이며, Widen이 여기에 포함되지 않는 이유도 이 때문입니다. 피연산자 레지스터에는 widen의 출력을 아예 쓸 수 없습니다. 따라서 4-way 영역에서 계산된 값을 스태시에 넣으려면 먼저 widen한 뒤 Clip 연산에서 스태시해야 합니다. 이렇게 하면 쓰기와 읽기가 모두 8-way 쪽에서 이루어집니다. 스태시는 Pair Mode에서 사용할 수 없으며, Inter-Slice Reducer로 넘어가는 IntraFirst 전환에서 제거됩니다. 따라서 vector_inter_slice_reduce() 이전에 스태시한 내용은 그 이후에 사라집니다.

그런 다음 인자 모드가 어느 슬롯에 스트림을 두고 어느 슬롯에 피연산자를 둘지 선택하므로, 같은 연산으로 예를 들어 stream + operand나 operand - stream을 계산할 수 있습니다. 예를 들어 BinaryArgMode::Mode10은 슬롯을 서로 바꾸어 SubFxp가 operand - stream을 계산하게 합니다.

.vector_fxp_with_mode(FxpBinaryOp::SubFxp, BinaryArgMode::Mode10, 7)  // computes 7 - stream

BinaryArgMode는 이항 연산의 두 슬롯 중 어느 쪽이 스트림이고 어느 쪽이 피연산자인지 선택합니다(단항 연산에는 모드가 없으며 항상 op(stream)으로 실행됩니다).

BinaryArgMode슬롯계산
Mode00스트림 / 스트림op(stream, stream)
Mode01스트림 / 피연산자op(stream, operand) (기본값)
Mode10피연산자 / 스트림op(operand, stream)
Mode11피연산자 / 피연산자op(operand, operand)

TernaryArgMode는 삼항 연산에 대해서도 같은 역할을 합니다:

TernaryArgMode슬롯계산
Mode012stream / operand0 / operand1op(stream, operand0, operand1) (기본값)
Mode002stream / stream / operand1op(stream, stream, operand1)
Mode102operand0 / stream / operand1op(operand0, stream, operand1)
Mode112operand0 / operand0 / operand1op(operand0, operand0, operand1)
Mode020stream / operand1 / streamop(stream, operand1, stream)
Mode021stream / operand1 / operand0op(stream, operand1, operand0)
Mode120operand0 / operand1 / streamop(operand0, operand1, stream)

Pair Mode

Pair Mode는 요소가 서로 인터리빙된 두 그룹으로 나뉘는 텐서에서 체인을 실행하므로, 연산이 두 그룹을 서로 연관 지을 수 있습니다(예: 쌍별 덧셈, 비대칭 스케일). 진입점은 vector_init() 바로 다음에 오는 vector_intra_slice_unzip()이며, 2-way 그룹화 축을 가진 수집된 텐서에 적용됩니다. 체인을 vector_intra_slice_unzip()으로 시작하면 하류의 Filter 단계를 사용할 수 없습니다. 내부적으로 vector_intra_slice_unzip()은 2-way 그룹화 축을 따른 위치로부터 각 요소의 GroupId를 도출하며, 이는 TagMode가 아니라 branch unit의 count augmenter를 구동합니다. 체인을 시작할 때 사용한 태그는 변경되지 않습니다. 이 때문에 그룹 분할에는 이 방법이 지원되는 경로이며 TagMode::AxisToggle은 지원되지 않습니다.

이 흐름은 네 단계로 이루어집니다:

  1. vector_intra_slice_unzip()는 입력을 두 개의 병렬 스트림(group 0과 group 1)으로 나눕니다.
  2. 체인은 두 그룹이 보조를 맞추면서 여러 단계를 거쳐 실행됩니다(paired 페이즈).
  3. _zip 연산이 두 스트림을 다시 하나로 합칩니다(merged 페이즈).
  4. 병합된 스트림은 일반 체인과 마찬가지로 vector_final()까지 이어집니다.

paired 페이즈 동안의 단계는 두 종류로 나뉩니다.

  • 공통 단계(vector_fxp_to_fp, vector_narrow_split, vector_widen_concat, vector_fp_to_fxp)는 두 그룹에 동일하게 작용합니다. vector_narrow_trim과 vector_widen_pad는 쌍에서 사용할 수 없으므로 대신 _split / _concat 변형을 사용하십시오.
  • 그룹별 연산은 그룹마다 인자를 하나씩 받습니다.
    • 이항 및 삼항 연산(vector_fxp, vector_fp_binary, vector_fp_ternary, vector_clip 등)은 한쪽에 ()를 넘겨 해당 쪽을 건너뛰거나, 양쪽에 서로 다른 피연산자를 받을 수 있습니다.
    • 단항 연산(vector_fp_unary)은 예외입니다. 이 연산은 플래그 (op, group0_apply, group1_apply)를 받으며, false이면 해당 그룹을 건너뜁니다.

Pair Mode는 연산에 따라 BinaryArgMode를 다르게 해석합니다. 그룹별 연산(vector_fxp_with_mode, vector_fp_binary_with_mode 등)은 각 그룹 안에서 독립적으로 모드를 적용하며(0은 해당 그룹의 스트림, 1은 해당 그룹의 피연산자), _zip 연산(vector_fxp_zip_with_mode 등)은 두 슬롯을 그룹으로 나뉜 두 스트림으로 받습니다(0은 Group 0의 스트림, 1은 Group 1의 스트림).

_zip BinaryArgMode슬롯계산
Mode00group0 / group0op(group0, group0)
Mode01group0 / group1op(group0, group1) (기본값)
Mode10group1 / group0op(group1, group0)
Mode11group1 / group1op(group1, group1)

페어 모드의 제약 조건은 다음과 같습니다.

  • stash()와 filter()는 Pair Mode 전체(쌍 단계와 병합 단계 모두)에서 사용할 수 없습니다.
  • vector_intra_slice_reduce() 역시 _zip 전후를 막론하고 Pair Mode 전체에서 사용할 수 없습니다. 리듀서가 그룹 조건을 받지 않기 때문입니다.
  • _zip 이전(paired 페이즈)에는 그룹별 텐서에서 vector_inter_slice_reduce()를 사용할 수 없으므로 체인이 Inter-Slice Reducer로 전환할 수 없습니다. _zip 이후(merged 페이즈)에는 결과가 다시 Commitable이 되며, 현재 단계가 해당 전환을 지원하면 vector_inter_slice_reduce()를 호출할 수 있습니다.
  • ALU 사용량은 두 그룹이 공유합니다. 어느 한 그룹에서 사용한 ALU는 두 그룹 모두에서 소모된 것으로 간주됩니다.

단계

하나의 단계 안에서 각 ALU는 Tensor Unit 호출 한 번당 최대 한 번만 실행됩니다. 이는 주로 여러 연산자가 단계 로컬 ALU 풀을 공유하는 Logic, Fxp, Fp, Clip에서 중요합니다. 예를 들어 tanh(sqrt(x))는 tanh와 sqrt가 모두 FpFpu ALU를 사용하므로 단일 Tensor Unit 호출에 들어갈 수 없습니다.

Tag

Tag 단계는 체인의 진입점으로, 플릿 안의 각 32비트 요소에 4비트 Tag(0-15)를 부여하며, 이후 단계에서는 이를 사용해 조건부 연산을 적용합니다. 비트 3(MSB)은 GroupId이며, Filter와 Pair Mode에서 요소를 Group 0 / Group 1로 나누는 데 사용됩니다. 비트 0..2는 비교 결과로 채워지는 범용 플래그 비트입니다.

TagMode는 각 요소에 대해 4개의 비트를 계산하는 방식을 선택합니다. 현재는 Zero와 Comparison만 사용할 수 있습니다. 마지막 열에 기록된 대로 AxisToggle은 선언되어 있지만 컴파일할 수 없으며, ValidCount와 Vrf는 두 경로 모두에서 실행될 때까지 열거형에서 아예 제외되어 있습니다.

TagMode각 태그 비트를 채우는 방식상태
Zero네 비트가 모두 0입니다. 모든 요소의 태그가 0입니다.동작함
Comparison([cmp0, cmp1, cmp2, cmp3])각 요소 x에 대해 cmp_i(x)가 성립하는 경우에만 비트 i = 1입니다. 각 cmp_i는 비교 대상 경계값을 담은 Cmp<D>이며, 스트림의 스칼라 타입으로 타입이 지정되므로 경계값은 그 타입에 맞는지 검사됩니다: Equal, Less, Greater, LessUnsigned, GreaterUnsigned, True, False. *Unsigned 쌍은 원시 비트 패턴을 비교합니다.동작함
AxisToggle { axis }비트 3(GroupId) = axis를 따라 axis_index % 2입니다. 비트 0..2는 0으로 유지됩니다.컴파일할 수 없습니다. 호스트에서는 실행되지만, lowering 시 커널 컴파일 오류로 거부됩니다. 이 그룹화를 얻는 지원되는 방법은 vector_intra_slice_unzip()입니다.

예를 들어 i32 데이터와 Comparison([Cmp::Less(0), Cmp::Equal(5), Cmp::Greater(100), Cmp::True])를 사용하면, 요소 x = 7은 비트 0/0/0/1(LSB부터)을 만들어 내므로 태그는 0b1000 = 8입니다.

태그가 할당되면 이후의 이항 및 삼항 연산은 태그에 따라 피연산자에 조건을 걸 수 있으므로, 요소마다 다른 값을 보게 됩니다. 규칙은 한 문장입니다. 조건이 없으면 피연산자를 그대로 쓰고, 조건이 있으면 Branched를 통해 그 피연산자가 구동하는 하드웨어 슬롯을 지정하십시오. furiosa-opt-examples의 ve_elementwise_branched는 바로 그렇게 하는 컴파일된 커널이며(조건이 걸린 즉시 값 하나와 그 뒤에 오는 무조건 rf 읽기), ve_branch_bit_order는 즉시 값 세 개와 조건이 걸린 스태시 읽기를 추가합니다. 두 커널은 여기서 다시 서술하지 않고 직접 읽어 오므로, API가 바뀌면 설명이 뒤처진 채 남는 대신 해당 커널이 깨집니다.

TagGuard는 TagGuard::all(), 하나의 그룹에 대한 TagGuard::group(id), 또는 LSB부터 작성하고 태그 비트마다 BitReq를 하나씩 두는 [BitReq; 4] 패턴의 matches/not_matches 중 하나입니다. 비트가 1이어야 하면 One, 0이어야 하면 Zero, 상관없으면 Ignore를 씁니다. 이 요구 조건은 태그 모드가 무엇으로 비트를 채웠든 비트의 값에 적용됩니다.

한 패스에는 피연산자 슬롯이 네 개 있습니다. 즉시 레지스터 세 개와 레지스터 파일 포트이며, 이 포트는 VRF 레지스터 또는 스태시를 읽습니다. 조건부 피연산자는 자신이 구동하는 슬롯의 종류를 지정합니다. 즉시값에는 Branched::imm, 포트에는 Branched::rf를 사용하며, 그 결과로 뒤에 이어질 수 있는 것만 제공하는 빌더를 돌려받습니다. imm은 세 번, rf는 마지막에 한 번 호출할 수 있으며, 네 번째 imm이나 rf 뒤의 imm은 컴파일되지 않습니다.

커널은 어느 즉시값 레지스터를 쓸지 고르지 않습니다. imm은 다음 레지스터를 가져가며, 값이 최종적으로 어느 물리 레지스터에 들어가는지는 명령 생성 단계가 결정합니다. 보존되는 것은 호출 순서입니다. 슬롯은 채워진 순서대로 하드웨어에 도달하고 그 순서가 곧 매치 우선순위입니다. 따라서 요소는 자신이 만족하는 가드를 가진 첫 번째 슬롯을 취하며, 무조건 슬롯은 뒤에 아무것도 오지 않는 패스의 else가 됩니다.

두 가지 가드는 곧바로 거부됩니다. 이러한 가드를 가진 슬롯은 자리만 차지하고 아무 동작도 하지 않는데, 패스에는 슬롯이 네 개뿐이기 때문입니다. 어떤 실행 id도 만족하지 않는 TagGuard::not_matches([Ignore, Ignore, Ignore, Ignore]), 그리고 무조건 슬롯 뒤에 놓인 모든 슬롯이 이에 해당합니다. TagGuard::matches([Ignore, Ignore, Ignore, Ignore])는 허용되며, 같은 조건을 더 짧게 표기한 TagGuard::all()로 저장됩니다.

레이아웃을 만들어 돌려주는 헬퍼는 일반 함수이므로, 커널 라이브러리는 자주 쓰는 조합을 무엇이든 감쌀 수 있습니다. 이러한 헬퍼가 할 수 없는 일이 두 가지 있으며, 둘 다 operand 모듈에 자세히 문서화되어 있습니다. 첫째, 새로운 피연산자 타입을 도입할 수 없습니다(피연산자는 스태시에 무엇을 하는지에 대한 선언을 담고 있으므로, 이를 나타내는 트레이트는 닫혀 있습니다). 둘째, 슬롯을 조건부로 채울 수 없습니다. 몇 개의 슬롯이 사용되었는지가 빌더 타입의 일부이므로 if의 두 갈래가 서로 일치하지 않게 되기 때문입니다. 슬롯 개수 대신 페이로드나 가드를 바꾸거나, 연산 전체를 분기로 감싸십시오.

Logic Cluster

Logic Cluster는 i32 또는 f32에 대해 비트 연산(비트 수준)을 수행합니다. 8-way로 동작합니다.

이 단계는 다섯 가지 ALU 클래스(LogicAnd, LogicOr, LogicXor, LogicLshift, LogicRshift)를 제공하며, 각 클래스는 Tensor Unit 호출당 한 번씩 실행할 수 있습니다. 같은 클래스를 공유하는 연산자들은 하나의 호출로 융합할 수 없습니다.

i32 연산:

연산ALU비고
BitAndLogicAnd비트 AND
BitOrLogicOr비트 OR
BitXorLogicXor비트 XOR
LeftShiftLogicLshift논리 왼쪽 시프트
LogicRightShiftLogicRshift논리 오른쪽 시프트
ArithRightShiftLogicRshift산술 오른쪽 시프트

f32 연산:

연산ALU비고
BitAndLogicAnd부동소수점 비트 패턴에 대한 비트 단위 AND
BitOrLogicOr부동소수점 비트 패턴에 대한 비트 단위 OR
BitXorLogicXor부동소수점 비트 패턴에 대한 비트 단위 XOR

Fxp Cluster

Fxp Cluster는 i32에 대해 정수 및 고정소수점 산술 연산을 수행합니다. 8-way로 동작합니다.

이 단계는 네 가지 ALU 클래스(FxpAdd, FxpLshift, FxpMul, FxpRshift)를 제공하며, 각 클래스는 Tensor Unit 호출당 한 번씩 실행할 수 있습니다. 같은 클래스를 공유하는 연산자들은 하나의 호출로 융합할 수 없습니다.

연산ALU비고
AddFxpFxpAdd래핑 덧셈
AddFxpSatFxpAdd포화 덧셈
SubFxpFxpAdd래핑 뺄셈
SubFxpSatFxpAdd포화 뺄셈
LeftShiftFxpLshift논리 왼쪽 시프트
LeftShiftSatFxpLshift포화 왼쪽 시프트
MulFxpFxpMul고정소수점 곱셈
MulIntFxpMul정수 곱셈
LogicRightShiftFxpRshift논리 오른쪽 시프트
ArithRightShiftFxpRshift산술 오른쪽 시프트
ArithRightShiftRoundFxpRshift반올림을 적용한 산술 오른쪽 시프트

예를 들어 단일 ALU 규칙은 둘 다 FxpAdd를 대상으로 하는 두 연산을 거부합니다:

// PANICS: "FxpAdd is already in use"
input
    .vector_init()
    .vector_intra_slice_tag(TagMode::Zero)
    .vector_fxp(FxpBinaryOp::AddFxp, 10)    // uses FxpAdd
    .vector_fxp(FxpBinaryOp::MulInt, 2)     // uses FxpMul ✓
    .vector_fxp(FxpBinaryOp::SubFxp, 5)     // uses FxpAdd again ✗
    .vector_final()

FxpToFp 변환

FxpToFp 변환 단계는 i32를 f32로 변환합니다. int_width 매개변수는 변환에 사용할 정수 비트 폭을 지정합니다. int_width = 31은 표준 i32 ↔ f32 변환입니다.

메서드효과
vector_fxp_to_fp(int_width)i32 스트림을 f32로 변환합니다

Narrow

Narrow 단계는 8-way를 4-way로 전환합니다. 8-way 패킷은 활성 요소 8개(Packet = m![... # 8])를, 4-way 패킷은 4개(Packet = m![... # 4])를 전달합니다. Narrowing을 하면 float 경로와 reduce 경로의 처리량이 절반으로 줄어들므로, 논리적 텐서 형태가 같아도 패킷 수나 Tensor Unit 호출 횟수가 두 배로 늘어납니다.

메서드사용 시점효과
vector_narrow_split()앞뒤 절반 모두에 실제 데이터가 있는 경우8-way 플릿 하나를 앞쪽 4개와 뒤쪽 4개 패킷으로 나누고 Time과 Packet을 갱신합니다
vector_narrow_trim()뒤쪽 4개 요소가 이미 패딩이거나 필요 없는 경우앞쪽 4개 요소만 남깁니다

형태 의미론:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2, S = 64];

fn vector_narrow_split_semantics<'l, const T: Tu>(
    input: VectorBranchTensor<'l, T, i32, m![1], m![B], m![S / 4 # 256], m![S % 4], m![A % 8], Fresh, { stage::VeOrder::IntraFirst }>,
) -> VectorNarrowTensor<'l, T, i32, m![1], m![B], m![S / 4 # 256], m![S % 4, A / 4 % 2], m![A % 4], Fresh, { stage::VeOrder::IntraFirst }>
{
    input.vector_narrow_split::<m![S % 4, A / 4 % 2], m![A % 4]>()
    // shape semantics: [T], [P] -> [T, P / 2], [P % 4]
}

fn vector_narrow_trim_semantics<'l, const T: Tu>(
    input: VectorBranchTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8], Fresh, { stage::VeOrder::IntraFirst }>,
) -> VectorNarrowTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 4], Fresh, { stage::VeOrder::IntraFirst }>
{
    input.vector_narrow_trim::<m![A % 2 # 4]>()
    // shape semantics: [T], [P] -> [T], [P = 4]
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: VectorBranchTensor<'_, _, i32, m![1], m![B], m![S / 4 # 256], m![S % 4], m![A % 8], Fresh, { stage::VeOrder::IntraFirst }> = VectorBranchTensor::new(&mut device.main, Tensor::zero(), TagMode::Zero);
let _o = vector_narrow_split_semantics(i);

let i: VectorBranchTensor<'_, _, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8], Fresh, { stage::VeOrder::IntraFirst }> = VectorBranchTensor::new(&mut device.main, Tensor::zero(), TagMode::Zero);
let _o = vector_narrow_trim_semantics(i);
}

Float Cluster

Float Cluster는 f32에 대한 단항, 이항, 삼항 부동소수점 연산을 제공합니다. 4-way로 동작하므로 입력이 이미 Narrow를 거친 상태여야 합니다.

서로 독립적인 ALU 5개(FpFma, FpFpu, FpExp, FpMul0, FpMul1)를 제공하며, 각 ALU는 Tensor Unit 호출 한 번에 한 번씩 실행할 수 있습니다. ALU 계획이 가장 중요한 단계가 바로 이 단계입니다.

단항 연산:

연산ALU비고
ExpFpExp지수 함수
NegExpFpExp음의 지수 함수
SqrtFpFpu제곱근
TanhFpFpu쌍곡탄젠트
SigmoidFpFpu시그모이드
ErfFpFpu오차 함수
LogFpFpu자연로그
SinFpFpu사인
CosFpFpu코사인

이항 연산:

연산ALU비고
AddFFpFma부동소수점 덧셈
SubFFpFma부동소수점 뺄셈
MulF(FpMulAlu::Mul0)FpMul0곱셈
MulF(FpMulAlu::Mul1)FpMul1곱셈
MulF(FpMulAlu::Fma)FpFma곱셈
DivFFpFpuFp 단계 내부의 나눗셈

삼항 연산:

연산ALU비고
FmaFFpFma융합 곱셈-덧셈(fused multiply-add)

예를 들어 exp(sqrt(((x + 1) * 2) * 3))를 계산하려면 다음과 같이 합니다.

  • FpFma(FpBinaryOp::AddF)로 x1 = x + 1 계산
  • FpMul0(FpBinaryOp::MulF(FpMulAlu::Mul0))으로 x2 = x1 * 2 계산
  • FpMul1(FpBinaryOp::MulF(FpMulAlu::Mul1))로 x3 = x2 * 3 계산
  • FpFpu(FpUnaryOp::Sqrt)로 x4 = sqrt(x3) 계산
  • FpExp(FpUnaryOp::Exp)로 x5 = exp(x4) 계산

IntraSliceReduce

IntraSliceReduce 단계는 단일 Slice 내의 축을 리덕션합니다. 이 단계는 4-way로 실행됩니다. 이 단계는 전용 누산기 트리(accumulator-tree) ALU를 사용하므로 사용자가 선택할 수 있는 ALU는 제공되지 않습니다.

데이터 타입지원 연산
i32AddSat, Max, Min
f32Add, Max, Min

자세한 내용은 Intra-Slice Reduce를 참고하십시오.

FpDiv

FpDiv 단계는 부동소수점 나눗셈을 수행합니다. 이 단계는 4-way로 동작합니다. 이 단계는 전용 부동소수점 나눗셈기를 사용하므로 사용자가 선택할 수 있는 ALU를 제공하지 않습니다.

연산비고
FpDivBinaryOp::DivF전용 부동소수점 나눗셈

Widen

Widen 단계는 4-way에서 다시 8-way로 전환합니다. 이후 단계(FpToFxp, Clip, Filter, Output)는 다시 8개 요소로 이루어진 패킷을 받습니다.

메서드사용 시점효과
vector_widen_concat()앞서 수행한 vector_narrow_split()을 되돌릴 때4-way 패킷 두 개를 다시 8-way 플릿 하나로 병합합니다
vector_widen_pad()앞서 수행한 vector_narrow_trim()을 되돌릴 때4-way 패킷을 유효하지 않은 채움 값으로 패딩하여 다시 8개 요소로 복원

형태 의미론:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2, S = 64, R = 8];

fn vector_widen_concat_semantics<'l, const T: Tu>(
    input: VectorIntraSliceReduceTensor<'l, T, i32, m![1], m![B], m![S / 4 # 256], m![A / 4 % 2], m![A % 4], Fresh, { stage::VeOrder::IntraFirst }>,
) -> VectorWidenTensor<'l, T, i32, m![1], m![B], m![S / 4 # 256], m![1], m![A % 8], Fresh, { stage::VeOrder::IntraFirst }>
{
    input.vector_widen_concat::<m![1], m![A % 8]>()
    // shape semantics: [T, P / 2], [P % 4] -> [T], [P]
}

fn vector_widen_pad_semantics<'l, const T: Tu>(
    input: VectorFpTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 4], Fresh, { stage::VeOrder::IntraFirst }>,
) -> VectorWidenTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8], Fresh, { stage::VeOrder::IntraFirst }>
{
    input.vector_widen_pad::<m![A % 2 # 8]>()
    // shape semantics: [T], [P] -> [T], [P # 8]
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: VectorBranchTensor<'_, _, i32, m![1], m![B], m![S / 4 # 256], m![R, A / 4 % 2], m![A % 4 # 8], Fresh, { stage::VeOrder::IntraFirst }> = VectorBranchTensor::new(&mut device.main, Tensor::zero(), TagMode::Zero);
let i = i
    .vector_narrow_trim::<m![A % 4]>()
    .vector_intra_slice_reduce::<R, m![A / 4 % 2], m![A % 4]>(IntraSliceReduceOpI32::AddSat);

let _o = vector_widen_concat_semantics(i);

let i: VectorBranchTensor<'_, _, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8], Fresh, { stage::VeOrder::IntraFirst }> = VectorBranchTensor::new(&mut device.main, Tensor::zero(), TagMode::Zero);
let i = i.vector_narrow_trim::<m![A % 2 # 4]>().vector_fp_unary(FpUnaryOp::Exp);
let _o = vector_widen_pad_semantics(i);
}

FpToFxp 변환

FpToFxp 변환 단계는 f32를 다시 i32로 변환합니다. int_width 매개변수는 정수 비트 폭을 지정합니다.

메서드효과
vector_fp_to_fxp(int_width)f32 스트림을 다시 i32로 변환

Clip Cluster

Clip Cluster는 클램핑 연산과 비교 연산을 수행합니다. 8-way로 동작합니다.

이 단계는 세 가지 ALU 클래스(ClipAdd, ClipMax, ClipMin)를 제공하며, 각 클래스는 Tensor Unit 호출 한 번당 한 번씩 실행할 수 있습니다.

i32 연산:

연산ALU비고
MinClipMin최솟값
MaxClipMax최댓값
AbsMinClipMin절댓값 기준 최솟값
AbsMaxClipMax절댓값 기준 최댓값
AddFxpClipAdd래핑 덧셈
AddFxpSatClipAdd포화 덧셈

f32 연산:

연산ALU비고
MinClipMin최솟값
MaxClipMax최댓값
AbsMinClipMin절댓값 기준 최솟값
AbsMaxClipMax절댓값 기준 최댓값
AddClipAdd부동소수점 덧셈

Filter

Filter 단계는 TagGuard에서 도출한 실행 마스크(일반적으로 각 요소 Tag의 GroupId MSB와 매칭)를 적용하여 출력 플릿을 필터링합니다. 8-way 및 Standalone 컨텍스트에서만 사용할 수 있습니다. 원본 impl은 CanTransitionTo<Filter>를 가진 모든 단계에 대해 VectorTensor에 정의되어 있으며, 여기에는 모든 intra-slice 단계와 InterSliceReduce가 포함됩니다.

출력

Output 단계에서 Vector Engine 파이프라인을 빠져나갑니다. 결과는 Cast Engine, Transpose Engine 또는 Commit Engine으로 이어질 수 있습니다.

Reinterpret

vector_reinterpret::<D2>()는 비트를 그대로 둔 채 스트림을 다른 32비트 스칼라로 다시 읽으므로, 1.0f32는 1이 아니라 0x3f80_0000이 됩니다. 모든 요소는 32비트이고 모든 Cluster는 실행하는 연산에서 피연산자 타입을 가져오므로, reinterpret는 명령어를 생성하지 않고 ALU를 점유하지 않으며 현재 단계와 way를 모두 그대로 유지합니다. 따라서 reinterpret는 같은 Cluster의 두 연산 사이를 포함하여 체인의 어느 위치에나 몇 번이든 나타날 수 있습니다.

reinterpret의 한 가지 활용 예는 f32에 대한 비트 단위 abs에서처럼 부동소수점 값에 비트 연산을 적용하는 것입니다. 값을 보존하는 변환에는 대신 FxpToFp / FpToFxp를 사용하십시오.

메서드효과
vector_reinterpret::<i32>()f32 스트림의 비트를 i32로 읽습니다
vector_reinterpret::<f32>()i32 스트림의 비트를 f32로 읽습니다

재해석은 뒤따르는 vector_stash()가 무엇을 쓰는지도 결정합니다. 스태시는 쓰기 시점에 스트림이 담고 있는 스칼라를 받기 때문입니다. 따라서 값을 다른 스칼라로 읽는 쪽에 전달하려면 먼저 reinterpret한 뒤 stash해야 합니다. 다른 스칼라로 읽으면 컴파일되지 않습니다.

예제

i32 파이프라인

분기 후 상수를 더하는 최소한의 i32 체인입니다. Fxp 단계는 8-way로 실행되므로 narrow나 widen이 필요하지 않습니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048, B = 2];

fn add_constant<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        .vector_fxp(FxpBinaryOp::AddFxp, 100)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = add_constant(i);
}

f32 파이프라인

vector_narrow_trim()은 부동소수점 연산 전에 텐서를 8-way에서 4-way로 변환하는 Narrow 단계입니다. vector_widen_pad()는 연산 후에 다시 8-way로 변환하는 Widen 단계입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2];

fn sigmoid<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        .vector_narrow_trim::<m![A % 2 # 4]>() // Narrow: Way8 -> Way4
        .vector_fp_unary(FpUnaryOp::Sigmoid)
        .vector_widen_pad::<m![A % 2 # 8]>() // Widen: Way4 -> Way8
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = sigmoid(i);
}

f32에 대한 비트 단위 Abs

부호 비트를 지우면 \(|x|\)이 되며, 이는 부동소수점 ALU 없이 얻을 수 있습니다. 이 연산이 필요한 단 하나의 op에서만 스트림을 i32로 읽으므로 마스크는 일반적인 i32 리터럴입니다. 두 재해석은 모두 비용이 들지 않으므로, 이 연산의 비용은 정확히 LogicAnd 하나입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2];

fn abs<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        .vector_reinterpret::<i32>() // read the bits as i32
        .vector_logic(LogicBinaryOpI32::BitAnd, 0x7fff_ffff) // clear the sign bit
        .vector_reinterpret::<f32>() // and back
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = abs(i);
}

단일 스트림 인자 모드

BinaryArgMode::Mode10은 스트림과 피연산자의 위치를 서로 바꾸므로, SubFxp는 기본값인 stream - operand 대신 operand - stream(여기서는 7 - x)을 계산합니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048, B = 2];

fn bias_minus_x<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        .vector_fxp_with_mode(FxpBinaryOp::SubFxp, BinaryArgMode::Mode10, 7) // compute 7 - x
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = bias_minus_x(i);
}

VRF 피연산자

미리 로드된 VRF 데이터를 피연산자로 사용합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048, B = 2, N = 256];

fn vrf_add<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8], m![N], m![A % 8]>,
    vrf: &VrfTensor<i32, m![1], m![B], m![A / 8], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8], m![N], m![A % 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        .vector_fxp(FxpBinaryOp::AddFxp, vrf)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, i32, m![1], m![B], m![A / 8], m![N], m![A % 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let v: VrfTensor<i32, m![1], m![B], m![A / 8], m![A % 8]> = VrfTensor::zero();
let _o = vrf_add(i, &v);
}

Fp 전용 경로에서의 스태시

이른 단계에서 스태시한 다음, 이후 Clip 연산에서 사용하십시오. 이는 max(2 * x, x)를 구현합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2];

fn residual_max<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()                                        // enter VE
        .vector_intra_slice_tag(TagMode::Zero) // start the intra-slice path
        .vector_stash()                                       // save original x
        .vector_narrow_trim::<m![A % 2 # 4]>()                  // narrow to Way4
        .vector_fp_binary(FpBinaryOp::MulF(FpMulAlu::Mul0), 2.0f32) // compute 2 * x
        .vector_widen_pad::<m![A % 2 # 8]>()                   // widen back to Way8
        .vector_clip(ClipBinaryOpF32::Max, Stash)             // max(2 * x, x)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = residual_max(i);
}

Fxp 전용 경로에서의 스태시

이른 단계에서 스태시한 다음, 이후 Clip 연산에서 사용하십시오. 이는 max(x + bias, x)를 구현합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048, B = 2];

fn stash_at_fxp<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> {
    input
        .vector_init()                                      // enter VE
        .vector_intra_slice_tag(TagMode::Zero) // start the intra-slice path
        .vector_stash()                                     // save original x
        .vector_fxp(FxpBinaryOp::AddFxp, 100)               // compute x + bias
        .vector_clip(ClipBinaryOpI32::Max, Stash)           // compute max(x + bias, x)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = stash_at_fxp(i);
}

좁히기와 넓히기를 가로지르는 스태시

좁히기 전에 스태시하고, 넓힌 후에 사용하십시오. 이는 max(sigmoid(x), x)를 계산합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2];

fn stash_across_narrow_widen<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()                                     // enter VE
        .vector_intra_slice_tag(TagMode::Zero) // start the intra-slice path
        .vector_stash()                                    // save x (Way8)
        .vector_narrow_trim::<m![A % 2 # 4]>()               // narrow to Way4
        .vector_fp_unary(FpUnaryOp::Sigmoid)               // compute sigmoid(x) in Way4
        .vector_widen_pad::<m![A % 2 # 8]>()                // widen back to Way8
        .vector_clip(ClipBinaryOpF32::Max, Stash)          // compute max(sigmoid(x), x)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = stash_across_narrow_widen(i);
}

Pair 덧셈

인터리브된 두 그룹을 정수 덧셈으로 zip합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048, B = 2, I = 2];

fn pair_add<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8], m![I], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> {
    input
        .vector_init()
        .vector_intra_slice_unzip::<I, m![1]>()
        .vector_clip_zip(ClipBinaryOpI32::AddFxp)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, i32, m![1], m![B], m![A / 8], m![I], m![A % 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = pair_add(i);
}

Pair 측별 전처리

비대칭 전처리는 zip 전에 그룹 0만 스케일링합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048, B = 2, I = 2];

fn pair_preprocess_one_side<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8], m![I], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8], m![1], m![A % 8]> {
    input
        .vector_init()
        .vector_intra_slice_unzip::<I, m![1]>()
        .vector_fxp(FxpBinaryOp::MulInt, 10, ())   // group 0 only
        .vector_clip_zip(ClipBinaryOpI32::AddFxp)
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, i32, m![1], m![B], m![A / 8], m![I], m![A % 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = pair_preprocess_one_side(i);
}

Zip을 사용하는 Pair 부동소수점 파이프라인

두 그룹 모두 부동소수점 경로(narrow -> fp -> zip -> widen)를 거칩니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2, I = 2];

fn pair_fp_mul_zip<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![I], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()
        .vector_intra_slice_unzip::<I, m![1]>()
        .vector_narrow_split::<m![1 # 2], m![A % 2 # 4]>()        // both groups: Way8 -> Way4
        .vector_fp_zip(FpBinaryOp::MulF(FpMulAlu::Mul0))   // group0 * group1 (Way4)
        .vector_widen_concat::<m![1], m![A % 2 # 8]>()           // Way4 -> Way8
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![I], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = pair_fp_mul_zip(i);
}

Pair 그룹별 전처리

zip하기 전에 각 그룹에 서로 다른 연산을 적용합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2, I = 2];

fn pair_asymmetric_preprocess<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![I], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()
        .vector_intra_slice_unzip::<I, m![1]>()
        .vector_narrow_split::<m![1 # 2], m![A % 2 # 4]>()
        .vector_fp_unary(FpUnaryOp::Exp, true, false)         // group 0: exp(x), group 1: skip
        .vector_fp_zip(FpBinaryOp::MulF(FpMulAlu::Mul0))   // exp(group0) * group1
        .vector_widen_concat::<m![1], m![A % 2 # 8]>()
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![I], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = pair_asymmetric_preprocess(i);
}

Pair Zip 인자 모드

BinaryArgMode::Mode10은 zip할 때 그룹화된 두 스트림을 서로 바꿉니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 512, B = 2, I = 2];

fn pair_sub_reverse<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![B], m![A / 2], m![I], m![A % 2 # 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![A / 2], m![1], m![A % 2 # 8]> {
    input
        .vector_init()
        .vector_intra_slice_unzip::<I, m![1]>()
        .vector_narrow_split::<m![1 # 2], m![A % 2 # 4]>()
        .vector_fp_zip_with_mode(FpBinaryOp::SubF, BinaryArgMode::Mode10) // compute group1 - group0
        .vector_widen_concat::<m![1], m![A % 2 # 8]>()
        .vector_final()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let i: CollectTensor<'_, _, f32, m![1], m![B], m![A / 2], m![I], m![A % 2 # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = pair_sub_reverse(i);
}

성능

Logic, Fxp, Clip Cluster에서는 처리량이 완전한 8-way(사이클당 8개 요소)입니다. Float Cluster는 4-way로 동작하며, 실제로는 float 경로를 감싸는 Narrow/Widen 때문에 유효 처리량이 절반으로 줄어듭니다.

지연 시간은 사용하는 ALU마다 1사이클씩 늘어납니다. 여러 ALU에 걸친 연산은 각 ALU의 지연 시간이 누적됩니다. 예를 들어 exp(sqrt(x))는 지연 시간이 2사이클 늘어납니다(sqrt에 FpFpu, exp에 FpExp).


원문 © FuriosaAI · Programming Tensor Contraction Processors · Apache License 2.0 · NOTICE
yeokja 한국어 번역 · Anthropic 사의 claude-sonnet-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다 · 이 문서는 원문을 수정한 비공식 번역본이며 Apache License 2.0으로 제공합니다.