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

Vector Engine

Vector Engine은 요소별 연산과 리덕션을 수행합니다. 활성화 함수(GELU, SiLU), 정규화(softmax, layer norm), 이항 연산, Slice 내부 및 Slice 간 리덕션 등이 그 예입니다.

이 엔진은 32비트 타입인 i32와 f32만 받습니다. 앞 단계의 Contraction Engine은 타입을 자동으로 확장합니다(bf16 곱은 f32로, i8 곱은 i32로 누적됩니다). 이 엔진을 거치지 않는 경우에는 Fetch Adapter가 타입 캐스트 어댑터로 입력을 확장해야 합니다.

인터페이스

한 번의 Tensor Unit 호출에서 Vector Engine 부분은 vector_init()부터 vector_final()까지의 메서드 체인입니다. 이 엔진은 두 하위 구성 요소로 이루어집니다. 하나는 Intra-Slice Chain(요소별 / 이항 / Slice별 리덕션 단계)이고, 다른 하나는 Inter-Slice Reducer(Cluster 내 256개 Slice에 걸쳐 리덕션)입니다. vector_init()와 vector_final() 사이에서는 체인만 실행하거나, 리듀서만 실행하거나, 둘 다 실행할 수 있습니다. 둘 다 실행하는 경우 순서는 IntraFirst(체인 다음 리듀서) 또는 InterFirst(리듀서 다음 체인)입니다.

Intra-Slice Chain은 vector_intra_slice_tag()로 진입하며, 입력에 두 개의 병렬 스트림으로 나눌 2-way 그룹화 축이 있는 경우에는 vector_intra_slice_unzip()로 진입합니다(Pair Mode 참조). 두 진입점 모두 vector_init() 직후 또는 Inter-Slice Reducer의 출력에서 실행됩니다. Inter-Slice Reducer는 vector_init() 직후 또는 호환되는 Intra-Slice 단계에서 vector_inter_slice_reduce()로 진입합니다. 단계별 API 설명은 Intra-Slice Chain과 Inter-Slice Reducer를 참조하십시오.

아래 시그니처는 vector_init() 쪽 진입 메서드만 다룹니다. 같은 이름의 메서드(vector_intra_slice_tag, vector_inter_slice_reduce)는 체인↔리듀서 전환을 위해 체인 텐서와 리듀서 텐서에도 있으며, 이는 하위 페이지에서 설명합니다.

impl<'l, const T: Tu, P: CanApplyVectorInit, D: VeScalar, Chip: M, Cluster: M, Slice: M, Time: M, Packet: M>
    TuTensor<'l, T, P, D, Chip, Cluster, Slice, Time, Packet>
{
    /// Initializes Vector Engine processing for this tensor.
    #[primitive(TuTensor::vector_init)]
    pub fn vector_init(self) -> VectorInitTensor<'l, T, D, Chip, Cluster, Slice, Time, Packet> {
        VectorInitTensor::new(self.device, self.inner)
    }
}

impl<'l, const T: Tu, D: VeScalar, Chip: M, Cluster: M, Slice: M, Time: M, Packet: M>
    VectorInitTensor<'l, T, D, Chip, Cluster, Slice, Time, Packet>
{
    /// Enters VE intra-slice pipeline (single stream).
    #[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 }> {
        VectorBranchTensor::new(self.device, self.inner, branch)
    }

    /// Enters VE intra-slice pipeline (two-group / unzip).
    #[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> {
        verify_vector_intra_slice_unzip::<I, Time, SplitTime, Packet>();
        VectorTensorPair::new::<I, Time>(self.device, self.inner)
    }
}

impl<'l, const T: Tu, Chip: M, Cluster: M, Slice: M, Time: M, Packet: M>
    VectorInitTensor<'l, T, i32, Chip, Cluster, Slice, Time, Packet>
{
    /// Performs inter-slice reduce for i32 as the first VE operation.
    #[primitive(VectorInitTensor::vector_inter_slice_reduce)]
    pub fn vector_inter_slice_reduce<OutSlice: M, OutTime: M>(
        self,
        op: InterSliceReduceOpI32,
    ) -> VectorInterSliceReduceTensor<'l, T, i32, Chip, Cluster, OutSlice, OutTime, Packet, { VeOrder::InterFirst }>
    {
        let reduced = self.inner.reduce(op.reduce_fn(), op.identity(), true);
        create_inter_slice_reduce_tensor(self.device, reduced)
    }
}

예제

다음은 Vector Engine 호출이 어떤 모습인지 감을 잡을 수 있도록 보여 주는 몇 가지 대표 예제입니다. 전체 API 안내는 하위 페이지인 Intra-Slice Chain과 Inter-Slice Reducer에서 다룹니다.

ReLU 활성화

이 패스는 ReLU를 요소별로 적용하여 \(output[b, k, m, n] = \max(input[b, k, m, n], 0)\)를 계산합니다.

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

// ReLU activation after batched matrix multiplication.
// Chain-only pass, so the reducer is skipped and the path trivially resolves to IntraFirst.
// Both clusters (B = 2) and all 256 slices (K) carry real data, no padding.
fn relu<'l, const T: Tu>(
    input: ContractTensor<'l, T, f32, m![1], m![B], m![K], m![M, N / 8], m![N % 8]>,
) -> VectorFinalTensor<'l, T, f32, m![1], m![B], m![K], m![M, N / 8], m![N % 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        // max(x, 0), the ReLU itself
        .vector_clip(ClipBinaryOpF32::Max, 0.0f32)
        .vector_final()
}

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

let c: ContractTensor<'_, _, f32, m![1], m![B], m![K], m![M, N / 8], m![N % 8]> = ContractTensor::new(&mut device.main, Tensor::zero());
let _o = relu(c);
}

ReLU 후 리덕션

이 패스는 Slice별로 ReLU를 적용한 다음 R에 걸쳐 리덕션하여 \(output[a, b] = \sum_{r \in R} \max(input[a, b, r], 0)\)를 얻습니다.

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

// Chain applies ReLU, then reducer sums across slices.
// IntraFirst shape (chain runs first, then reducer).
// Both clusters (B = 2), all 256 slices (A / 8 * R), and full Way8 packet (A % 8) carry real data.
fn relu_then_reduce<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8, R], m![1], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8, 1 # 4], m![1], m![A % 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        // max(x, 0), the ReLU
        .vector_clip(ClipBinaryOpI32::Max, 0)
        // sum across R slices
        .vector_inter_slice_reduce::<m![A / 8, 1 # 4], m![1]>(InterSliceReduceOpI32::AddSat)
        .vector_final()
}

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

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

리덕션 후 바이어스

이 패스는 R에 걸쳐 리덕션한 다음 상수 바이어스를 더하여 \(output[a, b] = \left(\sum_{r \in R} input[a, b, r]\right) + 100\)를 얻습니다.

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

// Reducer sums across slices, then chain adds a bias to the reduced result.
// InterFirst shape (reducer runs first, then chain).
// Both clusters (B = 2), all 256 slices, and full Way8 packet carry real data.
fn reduce_then_add<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![A / 8, R], m![1], m![A % 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![A / 8, 1 # 4], m![1], m![A % 8]> {
    input
        .vector_init()
        // sum across R slices
        .vector_inter_slice_reduce::<m![A / 8, 1 # 4], m![1]>(InterSliceReduceOpI32::AddSat)
        .vector_intra_slice_tag(TagMode::Zero)
        // add bias 100
        .vector_fxp(FxpBinaryOp::AddFxp, 100)
        .vector_final()
}

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

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

Slice 내부 및 Slice 간 리덕션

이 패스는 Slice 내부 리듀서(R의 Time 및 Packet 부분 담당)와 Inter-Slice Reducer(R의 Slice 부분 담당)를 결합하여 R을 완전히 리덕션합니다. einsum 형식은 BR -> B이며, 포화 덧셈을 사용합니다.

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

// R splits across Slice (R / 32 = 256), Time (R % 32 / 4 = 8), and Packet (R % 4, padded to 8 in Way8).
// Chain runs intra-slice reduce over R's Time and Packet portions, then the reducer collapses the Slice portion.
// IntraFirst shape (chain runs first, then reducer).
fn full_sum<'l, const T: Tu>(
    input: CollectTensor<'l, T, i32, m![1], m![B], m![R / 32], m![R % 32 / 4], m![R % 4 # 8]>,
) -> VectorFinalTensor<'l, T, i32, m![1], m![B], m![1 # 256], m![1], m![1 # 8]> {
    input
        .vector_init()
        .vector_intra_slice_tag(TagMode::Zero)
        // Way8 → Way4 (back 4 packet positions were padding)
        .vector_narrow_trim::<m![R % 4]>()
        // sum over R's Time and Packet portions
        .vector_intra_slice_reduce::<R, m![1], m![1 # 4]>(IntraSliceReduceOpI32::AddSat)
        // Way4 → Way8
        .vector_widen_pad::<m![1 # 8]>()
        // sum over R's Slice portion across all 256 slices
        .vector_inter_slice_reduce::<m![1 # 256], m![1]>(InterSliceReduceOpI32::AddSat)
        .vector_final()
}

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

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

쌍 덧셈

이 패스는 I를 따라 교차 배치된 두 그룹을 분리한 다음 쌍별로 더합니다. einsum 형식은 ABI -> AB입니다.

#![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];

// Pair-mode entry via unzip, then a zip op fuses the two streams with an add.
// Both clusters (B = 2), all 256 slices (A / 8), and full Way8 packet (A % 8) carry real data.
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()
        // split into group 0 and group 1 along I
        .vector_intra_slice_unzip::<I, m![1]>()
        // group0 + group1
        .vector_clip_zip(ClipBinaryOpI32::AddFxp)
        .vector_final()
}

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

let c: 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(c);
}

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