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

Transpose Engine

Transpose Engine은 Chip, Cluster, Slice 차원은 그대로 두고 Time 차원과 Packet 차원을 맞바꿉니다.

인터페이스

CollectTensor와 VectorFinalTensor는 모두 .transpose()를 제공합니다. VectorFinalTensor 진입점은 Vector Engine 출력을 Transpose Engine에 직접 공급합니다.

// `D: MaterializableScalar` (see its doc) excludes i5/i9 stagings from transpose.
impl<
    'l,
    const T: Tu,
    P: CanApplyTranspose,
    D: MaterializableScalar,
    Chip: M,
    Cluster: M,
    Slice: M,
    Time: M,
    Packet: M,
    B: Backend,
> TuTensor<'l, T, P, D, Chip, Cluster, Slice, Time, Packet, B>
{
    /// Performs the transpose operation.
    #[primitive(TuTensor::transpose)]
    pub fn transpose<OutTime: M, OutPacket: M>(
        self,
    ) -> TransposeTensor<'l, T, D, Chip, Cluster, Slice, OutTime, OutPacket, B> {
        verify_transpose::<D, Time, Packet, OutTime, OutPacket>();
        TransposeTensor::new(self.device, self.inner.transpose(false))
    }
}

커널 작성자는 OutTime과 OutPacket(출력 차원 레이아웃)을 선택하고, 컴파일러는 그 결과가 매개변수에 나열된 하드웨어 제약 조건을 만족하는지 검증합니다.

아래 예제는 너비 16인 각 행을 두 개의 입력 패킷(D = 2)에서 모아 구성한 8×16 i8 행렬을 전치합니다. 이 예제는 이 페이지의 나머지 부분에서 계속 사용하는 예제입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![P = 256, B = 2, C = 8, D = 2, E = 8];

fn basic_transpose<'l, const T: Tu>(
    input: CollectTensor<'l, T, i8, m![1], m![1 # 2], m![P], m![B, C, D], m![E # 32]>,
) -> TransposeTensor<'l, T, i8, m![1], m![1 # 2], m![P], m![B, D, E], m![C # 32]> {
    input.transpose()
}

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

let c: CollectTensor<'_, _, i8, m![1], m![1 # 2], m![P], m![B, C, D], m![E # 32]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = basic_transpose(c);
}

아키텍처

아래의 다섯 가지 전치 단계는 인터페이스의 예제를 사용해 설명합니다.

매개변수

valid_size는 Transpose Engine이 32바이트 입력 버스에서 사이클마다 읽는 유효 요소의 수이며, 모든 입력 플릿은 bit-width × valid_size 형태로 도착하고, 32바이트 플릿에서 남는 바이트는 패딩으로 취급되어 Unpack 단계에서 버려집니다. 8비트 및 16비트 요소의 경우, 엔진은 이중 입력 패킹을 선택하여 각 플릿에서 기본값인 8개 대신 16개의 유효 요소를 읽을 수 있습니다. 4비트 또는 32비트 요소에서는 이중 패킹을 지원하지 않습니다. 데이터는 CollectTensor::transpose()(Fetch → [Switch →] Collect → [Cast →] Transpose 이후) 또는 VectorFinalTensor::transpose()(Vector Engine에서 직접)를 통해 Transpose Engine에 도달합니다. Contraction Engine은 32b × 8만 내보내는 반면, Vector Engine과 Fetch Engine은 아래 표의 어떤 조합이든 내보냅니다.

in_cols, in_rows, out_rows는 커널 작성자가 선택한 OutTime과 OutPacket에 따라 정해집니다. 네 가지 모두 요소 크기에 따라 제약을 받습니다:

요소 크기단일 valid_size이중 valid_size최대 in_rows유효한 in_cols
4비트16지원하지 않음1616, 32
8비트81688, 16, 32
16비트81648, 16, 32
32비트8지원되지 않음28, 16, 32

이 문서 전체에서 사용하는 예제(i8이므로 valid_size = 8)에서 컴파일러는 다음 값을 도출합니다.

매개변수값비고
in_cols16수집된 패킷 D = 2개 × valid_size = 8
in_rows8C::SIZE
out_rows16D·E(= in_cols, 완전 활용)
                 in_cols                  in_rows # F
           ┌─────────────────┐         ┌──────────────────┐
           │ 12 13 14 15 ... │         │ 3  7  11 15  ... │
 in_rows   │ 8  9  10 11 ... │  ────►  │ 2  6  10 14  ... │  out_rows
           │ 4  5  6  7  ... │         │ 1  5  9  13  ... │
           │ 0  1  2  3  ... │         │ 0  4  8  12  ... │
           └─────────────────┘         └──────────────────┘
                data_in                      data_out

Unpack

32바이트 입력 패킷마다 유효한 요소가 valid_size개 들어 있습니다. Unpack 단계는 플릿의 나머지 부분을 패딩으로 보고 버립니다. 8비트 또는 16비트 패킷에 기본 8요소 접두부를 넘어서는 유효 요소가 있으면 컴파일러는 이중 패킹을 선택합니다. 이때도 선택된 valid_size는 유효 접두부 전체를 포함해야 하며, 나머지 레인은 모두 패딩이어야 합니다.

이 문서의 예제에서는 [C, D, E # 32] → [C, D, E]입니다.

예를 들어 i8 패킷 [B=16 # 32]는 이중 패킹을 사용하여 유효 요소 16개를 모두 한 사이클에 전달합니다. 이와 동등한 단일 패킹 레이아웃이라면 연속된 두 패킷에서 각각 요소를 8개씩 전달하게 됩니다. i4 패킷은 이중 형식을 요청할 수 없습니다. 지원되는 valid_size가 16 하나뿐이기 때문입니다.

Gather

입력 행렬의 한 행은 너비가 in_cols = packets_per_col × valid_size개 요소이며, 연속된 packets_per_col개 패킷, 즉 가장 안쪽의 시간 단계들로부터 조립됩니다. Gather 단계는 이 패킷들을 하나의 행으로 이어 붙이고, 그 위의 in_rows개 시간 단계가 쌓여 [in_rows × in_cols] 입력 행렬을 이룹니다. (Unpack과 Gather는 모두 엔진이 입력을 읽는 동안 일어나며, 별도로 버퍼링되는 패스가 아닙니다.)

실행 예제에서 가장 안쪽의 D = 2개 패킷이 각각 valid_size = 8을 제공하여 너비 in_cols = 16의 행을 이루고, 그 위의 C = 8개 시간 단계가 쌓여 [8 × 16] 입력 행렬을 이룹니다.

Transpose

행렬을 전치합니다: [in_rows × in_cols] → [in_cols × in_rows].

계속 사용하는 예제에서는 [C, D, E] → [D, E, C]입니다.

Trim

일부 입력 패킷이 valid_size보다 적은 유효 요소를 담고 있으면 전치된 행렬에 패딩 행이 생깁니다. Trim 단계는 이러한 행을 제거하여 [out_rows × in_rows]를 만들며, 이때 out_rows ≤ in_cols입니다.

계속 사용하는 예제에서는 [D, E, C] → [D, E, C]입니다(입력이 완전히 활용되므로 제거되는 행이 없습니다). Trim이 실제로 행을 버리는 경우는 작은 행렬 예제를 참고하십시오.

Align

전치된 행은 in_rows개 요소 너비이지만, DM 패킷은 32바이트여야 합니다. Align 단계는 각 행을 32바이트 플릿으로 패딩하여 [out_rows × (in_rows # F)] 형태를 만들며, 여기서 F는 D[F]가 32바이트가 되도록 선택됩니다.

계속 사용하는 예제에서는 [D, E, C] → [D, E, C # 32]입니다.

지연 시간

Note

공식은 먼저 성능을 읽어 보십시오.

계속 사용하는 예제에서는 in_cols = 16 ≤ 16이므로 더블 버퍼링이 선택됩니다. in_flits = 16, out_rows = 16, n = 2일 때 총 지연 시간은 16 + 1 × max(16, 16) + 16 = 48 사이클입니다.

예제

작은 행렬

이 예제는 out_rows < in_cols일 때 Trim 단계가 패딩 행을 버리는 과정을 보여 줍니다:

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

fn small_transpose<'l, const T: Tu>(
    input: CollectTensor<'l, T, i8, m![1], m![1 # 2], m![P], m![A], m![B # 32]>,
) -> TransposeTensor<'l, T, i8, m![1], m![1 # 2], m![P], m![B], m![A # 32]> {
    input.transpose()
}

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

let c: CollectTensor<'_, _, i8, m![1], m![1 # 2], m![P], m![A], m![B # 32]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = small_transpose(c);
}

매개변수:

매개변수값비고
in_cols8B::SIZE = 2, 8로 패딩
in_rows4A::SIZE
out_rows2B::SIZE

단계:

  • Unpack: [A, B # 32] → [A, B # 8].
  • Gather: [A, B # 8] → [A, B # 8] (packets_per_col = 1이므로 각 패킷이 이미 온전한 in_cols = 8 행입니다).
  • Transpose: [A, B # 8] → [B # 8, A].
  • Trim: [B # 8, A] → [B, A] (패딩된 행 6개가 잘려 나갑니다).
  • Align: [B, A] → [B, A # 32].

지연 시간: in_cols = 8 ≤ 16이므로 더블 버퍼링이 선택됩니다. in_flits = 4, out_rows = 2, n = 1일 때 총 사이클 수는 4 + 0 × max(4, 2) + 2 = 6입니다.

큰 열

이 예제는 in_cols > 16으로 만들어 싱글 버퍼링을 강제합니다. 이로 인해 입력과 출력이 겹칠 수 없으므로 총 사이클이 증가합니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![P = 256, B = 2, C = 8, D = 4, E = 8];

fn large_col_transpose<'l, const T: Tu>(
    input: CollectTensor<'l, T, i8, m![1], m![1 # 2], m![P], m![B, C, D], m![E # 32]>,
) -> TransposeTensor<'l, T, i8, m![1], m![1 # 2], m![P], m![B, D, E], m![C # 32]> {
    input.transpose()
}

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

let c: CollectTensor<'_, _, i8, m![1], m![1 # 2], m![P], m![B, C, D], m![E # 32]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = large_col_transpose(c);
}

매개변수:

매개변수값비고
in_cols32D::SIZE × E::SIZE
in_rows8C::SIZE
out_rows32D::SIZE × E::SIZE

단계:

  • Unpack: [C, D, E # 32] → [C, D, E].
  • Gather: 가장 안쪽의 D = 4개 패킷이 각 in_cols = 32 행을 이루고, C = 8개의 시간 단계가 쌓여 [8 × 32] 입력 행렬이 됩니다.
  • Transpose: [C, D, E] → [D, E, C].
  • Trim: [D, E, C] → [D, E, C] (잘려 나가는 행이 없습니다).
  • Align: [D, E, C] → [D, E, C # 32].

지연 시간: in_cols = 32 > 16이므로 싱글 버퍼링이 선택됩니다. in_flits = 32, out_rows = 32, n = 2(B)일 때 총 사이클 수는 2 × (32 + 32) = 128입니다.

16비트 데이터 타입

이 예제는 bf16을 사용합니다. 더 넓은 요소로 인해 최대 in_rows가 절반(8 대신 4)으로 줄고, 32바이트 출력 플릿이 16개 요소로 줄어듭니다(i8의 경우 32개):

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![P = 256, C = 8, D = 4, E = 8];

fn bf16_transpose<'l, const T: Tu>(
    input: CollectTensor<'l, T, bf16, m![1], m![1 # 2], m![P], m![C, D], m![E # 16]>,
) -> TransposeTensor<'l, T, bf16, m![1], m![1 # 2], m![P], m![C, E], m![D # 16]> {
    input.transpose()
}

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

let c: CollectTensor<'_, _, bf16, m![1], m![1 # 2], m![P], m![C, D], m![E # 16]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = bf16_transpose(c);
}

매개변수:

매개변수값비고
in_cols8E::SIZE
in_rows4D::SIZE
out_rows8E::SIZE

단계:

  • Unpack: [D, E # 16] → [D, E].
  • Gather: [D, E] → [D, E] (packets_per_col = 1, 각 패킷이 이미 in_cols = 8인 완전한 행입니다).
  • Transpose: [D, E] → [E, D].
  • Trim: [E, D] → [E, D] (잘라내는 행이 없습니다).
  • Align: [E, D] → [E, D # 16].

지연 시간: in_cols = 8 ≤ 16이므로 더블 버퍼링이 선택됩니다. in_flits = 4, out_rows = 8, n = 8(C)일 때 총 사이클 수는 4 + 7 × max(4, 8) + 8 = 68입니다.

4비트 데이터 타입

이 예제는 i4를 사용합니다. valid_size = 16으로 사이클당 요소 수가 두 배가 되고 최대 in_rows는 16(16 × 4비트 = 8바이트)으로 늘어나며, 32바이트 플릿은 64개 요소로 커집니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![P = 256, B = 4, C = 16, E = 16];

fn i4_transpose<'l, const T: Tu>(
    input: CollectTensor<'l, T, i4, m![1], m![1 # 2], m![P], m![B, C], m![E # 64]>,
) -> TransposeTensor<'l, T, i4, m![1], m![1 # 2], m![P], m![B, E], m![C # 64]> {
    input.transpose()
}

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

let c: CollectTensor<'_, _, i4, m![1], m![1 # 2], m![P], m![B, C], m![E # 64]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = i4_transpose(c);
}

매개변수:

매개변수값비고
in_cols16E::SIZE
in_rows16C::SIZE
out_rows16E::SIZE

단계:

  • Unpack: [C, E # 64] → [C, E].
  • Gather: [C, E] → [C, E] (packets_per_col = 1, 각 패킷이 이미 온전한 in_cols = 16 행입니다).
  • Transpose: [C, E] → [E, C].
  • Trim: [E, C] → [E, C] (잘라 내는 행이 없습니다).
  • Align: [E, C] → [E, C # 64].

지연 시간: in_cols = 16 ≤ 16이므로 더블 버퍼링이 선택됩니다. in_flits = 16, out_rows = 16, n = 4(B)일 때 총합은 16 + 3 × max(16, 16) + 16 = 80 사이클입니다.

32비트 데이터 타입

이 예제는 Contraction Engine이 출력하는 두 가지 32비트 형식(32b × 8, f32 또는 i32) 중 하나인 f32를 사용합니다. 더 넓은 요소로 인해 최대 in_rows가 2(2 × 4바이트 = 8바이트)로 줄고, 32바이트 플릿이 8개 요소로 줄어듭니다:

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![P = 256, B = 4, D = 2, E = 8];

fn f32_transpose<'l, const T: Tu>(
    input: CollectTensor<'l, T, f32, m![1], m![1 # 2], m![P], m![B, D], m![E # 8]>,
) -> TransposeTensor<'l, T, f32, m![1], m![1 # 2], m![P], m![B, E], m![D # 8]> {
    input.transpose()
}

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

let c: CollectTensor<'_, _, f32, m![1], m![1 # 2], m![P], m![B, D], m![E # 8]> = CollectTensor::new(&mut device.main, Tensor::zero());
let _o = f32_transpose(c);
}

매개변수:

매개변수값비고
in_cols8E::SIZE
in_rows2D::SIZE
out_rows8E::SIZE

단계:

  • Unpack: [D, E # 8] → [D, E].
  • Gather: [D, E] → [D, E] (packets_per_col = 1; 각 패킷이 이미 완전한 in_cols = 8 행입니다).
  • Transpose: [D, E] → [E, D].
  • Trim: [E, D] → [E, D] (잘라내는 행이 없습니다).
  • Align: [E, D] → [E, D # 8].

지연 시간: in_cols = 8 ≤ 16이므로 더블 버퍼링이 선택됩니다. in_flits = 2, out_rows = 8, n = 4(B)일 때 합계는 2 + 3 × max(2, 8) + 8 = 34 사이클입니다.

성능

버스트 하나는 n = OutTime::SIZE / out_rows회의 전치 반복을 실행합니다. 각 반복은 in_flits = in_rows × (in_cols / valid_size)개의 입력 플릿과 out_rows개의 출력 플릿을 이동합니다.

버퍼링 모드

Transpose Engine에는 내부 버퍼가 두 개 있으며, 각 버퍼는 16개의 열을 담습니다. 컴파일러는 in_cols ≤ 16일 때 더블 버퍼링을, 그 외에는 싱글 버퍼링을 선택합니다. 더블 버퍼링은 입력과 출력을 겹쳐 총 사이클 수를 줄이고, 싱글 버퍼링은 두 단계를 순차적으로 수행합니다.

싱글 버퍼링 지연 시간

버스트의 총 지연 시간은 n × (in_flits + out_rows)입니다. 두 버퍼를 함께 사용하므로 매 반복마다 입력과 출력 시간이 더해집니다.

더블 버퍼링 지연 시간

버스트의 총 지연 시간은 in_flits + (n - 1) × max(in_flits, out_rows) + out_rows이며, 다음 세 단계로 나뉩니다:

  • 입력 전용 단계(in_flits 사이클): 첫 번째 버퍼가 채워집니다.
  • 중첩 단계((n - 1) × max(in_flits, out_rows) 사이클): 한 버퍼가 입력을 받는 동안 다른 버퍼가 동시에 출력을 생성하므로, 매 반복은 더 느린 쪽에 의해 결정됩니다.
  • 출력 전용 단계(out_rows 사이클): 마지막 버퍼가 비워집니다.

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